ваше сообщение коммита
This commit is contained in:
@@ -44,10 +44,10 @@ COPY package.json yarn.lock ./
|
||||
RUN yarn config set npmRegistryServer https://registry.npmjs.org \
|
||||
&& yarn config set registry https://registry.npmjs.org \
|
||||
&& yarn config set network-timeout 600000 \
|
||||
&& yarn install --frozen-lockfile
|
||||
&& yarn install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["yarn", "run", "dev"]
|
||||
CMD ["yarn", "run", "start"]
|
||||
@@ -59,7 +59,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
uint256 deadline; // конец периода голосования (sec)
|
||||
address initiator;
|
||||
bytes operation; // операция для исполнения
|
||||
uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
|
||||
uint256[] targetChains; // целевые сети для исполнения
|
||||
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
|
||||
mapping(address => bool) hasVoted;
|
||||
@@ -106,7 +105,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
event ProposalExecuted(uint256 proposalId, bytes operation);
|
||||
event ProposalCancelled(uint256 proposalId, string reason);
|
||||
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
|
||||
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
|
||||
event ModuleAdded(bytes32 moduleId, address moduleAddress);
|
||||
event ModuleRemoved(bytes32 moduleId);
|
||||
event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId);
|
||||
@@ -114,7 +112,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
event ChainRemoved(uint256 chainId);
|
||||
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp);
|
||||
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 LogoURIUpdated(string oldURI, string newURI);
|
||||
@@ -143,6 +141,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
error ErrNoPower();
|
||||
error ErrNotReady();
|
||||
error ErrNotInitiator();
|
||||
error ErrUnauthorized();
|
||||
error ErrLowPower();
|
||||
error ErrBadTarget();
|
||||
error ErrBadSig1271();
|
||||
@@ -232,25 +231,22 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
emit LogoURIUpdated(old, _logoURI);
|
||||
}
|
||||
|
||||
// Создать предложение с выбором цепочки для кворума
|
||||
// Создать предложение для multi-chain голосования
|
||||
function createProposal(
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes memory _operation,
|
||||
uint256 _governanceChainId,
|
||||
uint256[] memory _targetChains,
|
||||
uint256 /* _timelockDelay */
|
||||
) external returns (uint256) {
|
||||
if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
|
||||
if (_duration < minVotingDuration) revert ErrTooShort();
|
||||
if (_duration > maxVotingDuration) revert ErrTooLong();
|
||||
if (!supportedChains[_governanceChainId]) revert ErrBadChain();
|
||||
// _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
|
||||
return _createProposalInternal(
|
||||
_description,
|
||||
_duration,
|
||||
_operation,
|
||||
_governanceChainId,
|
||||
_targetChains,
|
||||
msg.sender
|
||||
);
|
||||
@@ -260,7 +256,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes memory _operation,
|
||||
uint256 _governanceChainId,
|
||||
uint256[] memory _targetChains,
|
||||
address _initiator
|
||||
) internal returns (uint256) {
|
||||
@@ -275,7 +270,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = _initiator;
|
||||
proposal.operation = _operation;
|
||||
proposal.governanceChainId = _governanceChainId;
|
||||
|
||||
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
|
||||
uint256 nowClock = clock();
|
||||
@@ -289,7 +283,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
|
||||
allProposalIds.push(proposalId);
|
||||
emit ProposalCreated(proposalId, _initiator, _description);
|
||||
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
|
||||
emit ProposalTargetsSet(proposalId, _targetChains);
|
||||
return proposalId;
|
||||
}
|
||||
@@ -352,7 +345,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
proposal.executed = true;
|
||||
|
||||
// Исполняем операцию
|
||||
_executeOperation(proposal.operation);
|
||||
_executeOperation(_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();
|
||||
|
||||
proposal.executed = true;
|
||||
_executeOperation(proposal.operation);
|
||||
_executeOperation(_proposalId, proposal.operation);
|
||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||
emit ProposalExecutionApprovedInChain(_proposalId, block.chainid);
|
||||
|
||||
@@ -489,11 +482,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
|
||||
/**
|
||||
* @dev Исполнить операцию
|
||||
* @param _proposalId ID предложения
|
||||
* @param _operation Операция для исполнения
|
||||
*/
|
||||
function _executeOperation(bytes memory _operation) internal {
|
||||
function _executeOperation(uint256 _proposalId, bytes memory _operation) internal {
|
||||
if (_operation.length < 4) revert ErrInvalidOperation();
|
||||
|
||||
// Получаем информацию о предложении для доступа к initiator
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
|
||||
// Декодируем операцию из formата abi.encodeWithSelector
|
||||
bytes4 selector;
|
||||
bytes memory data;
|
||||
@@ -527,10 +524,12 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
} else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) {
|
||||
(uint256 chainIdToRemove) = abi.decode(data, (uint256));
|
||||
_removeSupportedChain(chainIdToRemove);
|
||||
} else if (selector == bytes4(keccak256("_transferTokens(address,uint256)"))) {
|
||||
// Операция перевода токенов через governance
|
||||
(address recipient, uint256 amount) = abi.decode(data, (address, uint256));
|
||||
_transferTokens(recipient, amount);
|
||||
} else if (selector == bytes4(keccak256("_transferTokens(address,address,uint256)"))) {
|
||||
// Операция перевода токенов через governance от инициатора
|
||||
(address sender, address recipient, uint256 amount) = abi.decode(data, (address, address, uint256));
|
||||
// Проверяем, что sender совпадает с инициатором предложения
|
||||
if (sender != proposal.initiator) revert ErrUnauthorized();
|
||||
_transferTokens(sender, recipient, amount);
|
||||
} else if (selector == bytes4(keccak256("_updateVotingDurations(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 _amount Количество токенов для перевода
|
||||
*/
|
||||
function _transferTokens(address _recipient, uint256 _amount) internal {
|
||||
function _transferTokens(address _sender, address _recipient, uint256 _amount) internal {
|
||||
if (_recipient == address(0)) revert ErrZeroAddress();
|
||||
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,
|
||||
_duration,
|
||||
operation,
|
||||
_chainId,
|
||||
targets,
|
||||
msg.sender
|
||||
);
|
||||
@@ -732,7 +730,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
_description,
|
||||
_duration,
|
||||
operation,
|
||||
_chainId,
|
||||
targets,
|
||||
msg.sender
|
||||
);
|
||||
@@ -959,7 +956,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
bool canceled,
|
||||
uint256 deadline,
|
||||
address initiator,
|
||||
uint256 governanceChainId,
|
||||
uint256 snapshotTimepoint,
|
||||
uint256[] memory targetChains
|
||||
) {
|
||||
@@ -975,7 +971,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
p.canceled,
|
||||
p.deadline,
|
||||
p.initiator,
|
||||
p.governanceChainId,
|
||||
p.snapshotTimepoint,
|
||||
p.targetChains
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5482,7 +5482,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
uint256 deadline; // конец периода голосования (sec)
|
||||
address initiator;
|
||||
bytes operation; // операция для исполнения
|
||||
uint256 governanceChainId; // сеть голосования (Single-Chain Governance)
|
||||
uint256[] targetChains; // целевые сети для исполнения
|
||||
uint256 snapshotTimepoint; // блок/временная точка для getPastVotes
|
||||
mapping(address => bool) hasVoted;
|
||||
@@ -5529,7 +5528,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
event ProposalExecuted(uint256 proposalId, bytes operation);
|
||||
event ProposalCancelled(uint256 proposalId, string reason);
|
||||
event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains);
|
||||
event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId);
|
||||
event ModuleAdded(bytes32 moduleId, address moduleAddress);
|
||||
event ModuleRemoved(bytes32 moduleId);
|
||||
event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId);
|
||||
@@ -5537,7 +5535,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
event ChainRemoved(uint256 chainId);
|
||||
event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp);
|
||||
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 LogoURIUpdated(string oldURI, string newURI);
|
||||
@@ -5566,6 +5564,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
error ErrNoPower();
|
||||
error ErrNotReady();
|
||||
error ErrNotInitiator();
|
||||
error ErrUnauthorized();
|
||||
error ErrLowPower();
|
||||
error ErrBadTarget();
|
||||
error ErrBadSig1271();
|
||||
@@ -5655,25 +5654,22 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
emit LogoURIUpdated(old, _logoURI);
|
||||
}
|
||||
|
||||
// Создать предложение с выбором цепочки для кворума
|
||||
// Создать предложение для multi-chain голосования
|
||||
function createProposal(
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes memory _operation,
|
||||
uint256 _governanceChainId,
|
||||
uint256[] memory _targetChains,
|
||||
uint256 /* _timelockDelay */
|
||||
) external returns (uint256) {
|
||||
if (balanceOf(msg.sender) == 0) revert ErrNotHolder();
|
||||
if (_duration < minVotingDuration) revert ErrTooShort();
|
||||
if (_duration > maxVotingDuration) revert ErrTooLong();
|
||||
if (!supportedChains[_governanceChainId]) revert ErrBadChain();
|
||||
// _timelockDelay параметр игнорируется; timelock вынесем в отдельный модуль
|
||||
return _createProposalInternal(
|
||||
_description,
|
||||
_duration,
|
||||
_operation,
|
||||
_governanceChainId,
|
||||
_targetChains,
|
||||
msg.sender
|
||||
);
|
||||
@@ -5683,7 +5679,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
string memory _description,
|
||||
uint256 _duration,
|
||||
bytes memory _operation,
|
||||
uint256 _governanceChainId,
|
||||
uint256[] memory _targetChains,
|
||||
address _initiator
|
||||
) internal returns (uint256) {
|
||||
@@ -5698,7 +5693,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
proposal.deadline = block.timestamp + _duration;
|
||||
proposal.initiator = _initiator;
|
||||
proposal.operation = _operation;
|
||||
proposal.governanceChainId = _governanceChainId;
|
||||
|
||||
// Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке
|
||||
uint256 nowClock = clock();
|
||||
@@ -5712,7 +5706,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
|
||||
allProposalIds.push(proposalId);
|
||||
emit ProposalCreated(proposalId, _initiator, _description);
|
||||
emit ProposalGovernanceChainSet(proposalId, _governanceChainId);
|
||||
emit ProposalTargetsSet(proposalId, _targetChains);
|
||||
return proposalId;
|
||||
}
|
||||
@@ -5775,7 +5768,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
proposal.executed = true;
|
||||
|
||||
// Исполняем операцию
|
||||
_executeOperation(proposal.operation);
|
||||
_executeOperation(_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();
|
||||
|
||||
proposal.executed = true;
|
||||
_executeOperation(proposal.operation);
|
||||
_executeOperation(_proposalId, proposal.operation);
|
||||
emit ProposalExecuted(_proposalId, proposal.operation);
|
||||
emit ProposalExecutionApprovedInChain(_proposalId, block.chainid);
|
||||
|
||||
@@ -5912,11 +5905,15 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
|
||||
/**
|
||||
* @dev Исполнить операцию
|
||||
* @param _proposalId ID предложения
|
||||
* @param _operation Операция для исполнения
|
||||
*/
|
||||
function _executeOperation(bytes memory _operation) internal {
|
||||
function _executeOperation(uint256 _proposalId, bytes memory _operation) internal {
|
||||
if (_operation.length < 4) revert ErrInvalidOperation();
|
||||
|
||||
// Получаем информацию о предложении для доступа к initiator
|
||||
Proposal storage proposal = proposals[_proposalId];
|
||||
|
||||
// Декодируем операцию из formата abi.encodeWithSelector
|
||||
bytes4 selector;
|
||||
bytes memory data;
|
||||
@@ -5950,10 +5947,12 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
} else if (selector == bytes4(keccak256("_removeSupportedChain(uint256)"))) {
|
||||
(uint256 chainIdToRemove) = abi.decode(data, (uint256));
|
||||
_removeSupportedChain(chainIdToRemove);
|
||||
} else if (selector == bytes4(keccak256("_transferTokens(address,uint256)"))) {
|
||||
// Операция перевода токенов через governance
|
||||
(address recipient, uint256 amount) = abi.decode(data, (address, uint256));
|
||||
_transferTokens(recipient, amount);
|
||||
} else if (selector == bytes4(keccak256("_transferTokens(address,address,uint256)"))) {
|
||||
// Операция перевода токенов через governance от инициатора
|
||||
(address sender, address recipient, uint256 amount) = abi.decode(data, (address, address, uint256));
|
||||
// Проверяем, что sender совпадает с инициатором предложения
|
||||
if (sender != proposal.initiator) revert ErrUnauthorized();
|
||||
_transferTokens(sender, recipient, amount);
|
||||
} else if (selector == bytes4(keccak256("_updateVotingDurations(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 _amount Количество токенов для перевода
|
||||
*/
|
||||
function _transferTokens(address _recipient, uint256 _amount) internal {
|
||||
function _transferTokens(address _sender, address _recipient, uint256 _amount) internal {
|
||||
if (_recipient == address(0)) revert ErrZeroAddress();
|
||||
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,
|
||||
_duration,
|
||||
operation,
|
||||
_chainId,
|
||||
targets,
|
||||
msg.sender
|
||||
);
|
||||
@@ -6155,7 +6153,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
_description,
|
||||
_duration,
|
||||
operation,
|
||||
_chainId,
|
||||
targets,
|
||||
msg.sender
|
||||
);
|
||||
@@ -6382,7 +6379,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
bool canceled,
|
||||
uint256 deadline,
|
||||
address initiator,
|
||||
uint256 governanceChainId,
|
||||
uint256 snapshotTimepoint,
|
||||
uint256[] memory targetChains
|
||||
) {
|
||||
@@ -6398,7 +6394,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta
|
||||
p.canceled,
|
||||
p.deadline,
|
||||
p.initiator,
|
||||
p.governanceChainId,
|
||||
p.snapshotTimepoint,
|
||||
p.targetChains
|
||||
);
|
||||
|
||||
@@ -77,9 +77,8 @@
|
||||
"utf7": "^1.0.2",
|
||||
"viem": "^2.23.15",
|
||||
"winston": "^3.17.0",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ws": "^8.18.1",
|
||||
"hardhat": "^2.24.1",
|
||||
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
|
||||
"@nomicfoundation/hardhat-ethers": "^3.0.0",
|
||||
"@nomicfoundation/hardhat-ignition": "^0.15.10",
|
||||
@@ -87,9 +86,13 @@
|
||||
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
|
||||
"@nomicfoundation/hardhat-toolbox": "^5.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",
|
||||
"@typechain/ethers-v6": "^0.5.0",
|
||||
"@typechain/hardhat": "^9.0.0",
|
||||
"@types/chai": "^4.2.0",
|
||||
"@types/minimatch": "^6.0.0",
|
||||
"@types/mocha": ">=9.1.0",
|
||||
@@ -98,9 +101,6 @@
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"globals": "^16.0.0",
|
||||
"hardhat": "^2.24.1",
|
||||
"hardhat-contract-sizer": "^2.10.1",
|
||||
"hardhat-gas-reporter": "^2.2.2",
|
||||
"minimatch": "^10.0.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.5.3",
|
||||
|
||||
@@ -60,7 +60,7 @@ router.post('/get-proposals', async (req, res) => {
|
||||
return;
|
||||
}
|
||||
if (rpcUrl) {
|
||||
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId));
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
const dleAbi = [
|
||||
"function getSupportedChainCount() external view returns (uint256)",
|
||||
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
|
||||
@@ -97,7 +97,7 @@ router.post('/get-proposals', async (req, res) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId));
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
// ABI для чтения предложений (используем getProposalSummary для мультиконтрактов)
|
||||
const dleAbi = [
|
||||
@@ -369,7 +369,7 @@ router.post('/get-proposal-info', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const provider = new ethers.JsonRpcProvider(await rpcProviderService.getRpcUrlByChainId(chainId));
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||
|
||||
// ABI для чтения информации о предложении
|
||||
const dleAbi = [
|
||||
@@ -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 = [
|
||||
"function getProposalState(uint256 _proposalId) public view returns (uint8 state)"
|
||||
@@ -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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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 = [
|
||||
"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
|
||||
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 секунд таймаут
|
||||
@@ -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 секунд таймаут
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
* @desc Удалить DLE v2 по адресу
|
||||
|
||||
@@ -56,7 +56,11 @@ function formatABI(abi) {
|
||||
|
||||
// Функции
|
||||
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 returns = outputs ? ` returns (${outputs})` : '';
|
||||
|
||||
|
||||
@@ -175,7 +175,8 @@ class UnifiedDeploymentService {
|
||||
|
||||
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, '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -378,6 +379,15 @@ class UnifiedDeploymentService {
|
||||
return await this.deployParamsService.getAllDeployments();
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет параметры деплоя по deploymentId
|
||||
* @param {string} deploymentId - ID деплоя
|
||||
* @returns {boolean} - Успешность удаления
|
||||
*/
|
||||
async deleteDeployParams(deploymentId) {
|
||||
return await this.deployParamsService.deleteDeployParams(deploymentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все DLE из файлов (для совместимости)
|
||||
* @returns {Array} - Список DLE
|
||||
|
||||
@@ -31,11 +31,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
|
||||
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":
|
||||
version "1.5.0"
|
||||
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"
|
||||
integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==
|
||||
|
||||
"@langchain/community@^0.3.56":
|
||||
"@langchain/community@^0.3.34":
|
||||
version "0.3.59"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.59.tgz#9c64d0e08b69436845ba5ca4afb510c26dae1f32"
|
||||
integrity sha512-lYoVFC9wArWMXaixDgIadTE22jk4ZYAvSHHmwaMRagkGr5f4kyqMeJ83UUeW76XPx2cBy2fRSO+acSgqSuWE6A==
|
||||
@@ -510,25 +505,24 @@
|
||||
uuid "^10.0.0"
|
||||
zod "^3.25.32"
|
||||
|
||||
"@langchain/core@^0.3.80":
|
||||
version "0.3.80"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.80.tgz#c494a6944e53ab28bf32dc531e257b17cfc8f797"
|
||||
integrity sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA==
|
||||
"@langchain/core@0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.3.0.tgz#52bcf9d0bc480d2b2a456ee4aa8aed1cce6f6aba"
|
||||
integrity sha512-uYuozr9cHpm+Aat6RdheGWAiJ2GEmb/N33FCbHlN/+vKTwRmaju2F5pZi2CioK9kQwrQZVNydCbgaZm1c6ry6w==
|
||||
dependencies:
|
||||
"@cfworker/json-schema" "^4.0.2"
|
||||
ansi-styles "^5.0.0"
|
||||
camelcase "6"
|
||||
decamelize "1.2.0"
|
||||
js-tiktoken "^1.0.12"
|
||||
langsmith "^0.3.67"
|
||||
langsmith "^0.1.43"
|
||||
mustache "^4.2.0"
|
||||
p-queue "^6.6.2"
|
||||
p-retry "4"
|
||||
uuid "^10.0.0"
|
||||
zod "^3.25.32"
|
||||
zod "^3.22.4"
|
||||
zod-to-json-schema "^3.22.3"
|
||||
|
||||
"@langchain/ollama@^0.2.4":
|
||||
"@langchain/ollama@^0.2.0":
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.4.tgz#91c2108015e018f1dcae1207c8bc44da0cf047fa"
|
||||
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==
|
||||
|
||||
basic-ftp@^5.0.2:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0"
|
||||
integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.1.0.tgz#00eb8128ce536aa697c45716c739bf38e8d890f5"
|
||||
integrity sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.2"
|
||||
@@ -2233,6 +2227,11 @@ command-line-usage@^6.1.0:
|
||||
table-layout "^1.0.2"
|
||||
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:
|
||||
version "8.3.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.3.37.tgz#6931ee5af763a6df35c0ac467eab028ba0ad17de"
|
||||
integrity sha512-1jPsZ6xsxkcQPUvqRjvfuOLwZLLyt49hzcOK7OYAJovIkkOxd5gzK4Yw6giPUQ8g4XHyvULNlWBz+subdkcokw==
|
||||
@@ -4605,6 +4604,18 @@ kuler@^2.0.0:
|
||||
yaml "^2.2.1"
|
||||
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:
|
||||
version "0.3.87"
|
||||
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"
|
||||
integrity sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==
|
||||
|
||||
nodemailer@^7.0.11:
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.12.tgz#b6b7bb05566c6c8458ee360aa30a407a478d35b7"
|
||||
integrity sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==
|
||||
nodemailer@^6.10.0:
|
||||
version "6.10.1"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.1.tgz#cbc434c54238f83a51c07eabd04e2b3e832da623"
|
||||
integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==
|
||||
|
||||
nodemon@^3.1.11:
|
||||
nodemon@^3.1.9:
|
||||
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==
|
||||
dependencies:
|
||||
chokidar "^3.5.2"
|
||||
@@ -7532,11 +7543,11 @@ zip-stream@^6.0.1:
|
||||
readable-stream "^4.0.0"
|
||||
|
||||
zod-to-json-schema@^3.22.3:
|
||||
version "3.25.0"
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz#df504c957c4fb0feff467c74d03e6aab0b013e1c"
|
||||
integrity sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34"
|
||||
integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
|
||||
|
||||
217
docs/TASK_REQUIREMENTS.md
Normal file
217
docs/TASK_REQUIREMENTS.md
Normal 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 с криптографическими доказательствами, обеспечивая баланс между удобством использования и безопасностью.
|
||||
@@ -29,7 +29,7 @@ RUN apt-get update && apt-get install -y \
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn install
|
||||
|
||||
# Копируем остальные файлы проекта
|
||||
COPY . .
|
||||
|
||||
@@ -6,7 +6,7 @@ WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn install
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component
|
||||
:is="Component"
|
||||
:isAuthenticated="auth.isAuthenticated.value"
|
||||
:identities="auth.identities.value"
|
||||
:isAuthenticated="auth.isAuthenticated"
|
||||
:identities="auth.identities"
|
||||
:tokenBalances="tokenBalances"
|
||||
:isLoadingTokens="isLoadingTokens"
|
||||
:formattedLastUpdate="formattedLastUpdate"
|
||||
|
||||
@@ -15,12 +15,28 @@ import { getProposals } from '@/services/proposalsService';
|
||||
import { ethers } from 'ethers';
|
||||
import { useProposalValidation } from './useProposalValidation';
|
||||
import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract';
|
||||
import axios from 'axios';
|
||||
|
||||
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
|
||||
// Функция checkTokenBalance перенесена в useDleContract.js
|
||||
|
||||
// Функция 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) {
|
||||
const proposals = ref([]);
|
||||
const filteredProposals = ref([]);
|
||||
@@ -43,38 +59,85 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
||||
} = useProposalValidation();
|
||||
|
||||
const loadProposals = async () => {
|
||||
if (!dleAddress.value) {
|
||||
console.warn('Адрес DLE не найден');
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// Получаем информацию о всех DLE в разных цепочках
|
||||
console.log('[Proposals] Получаем информацию о всех DLE...');
|
||||
const dleResponse = await axios.get('/api/dle-v2');
|
||||
|
||||
if (!dleResponse.data.success) {
|
||||
console.error('Не удалось получить список DLE');
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
isLoading.value = true;
|
||||
const response = await getProposals(dleAddress.value);
|
||||
console.log(`[Proposals] Загружаем предложения из цепочки ${network.chainId}, адрес: ${network.address}`);
|
||||
const response = await getProposals(network.address);
|
||||
|
||||
if (response.success) {
|
||||
const rawProposals = response.data.proposals || [];
|
||||
const chainProposals = response.data.proposals || [];
|
||||
|
||||
console.log(`[Proposals] Загружено предложений: ${rawProposals.length}`);
|
||||
console.log(`[Proposals] Полные данные из блокчейна:`, rawProposals);
|
||||
// Добавляем информацию о цепочке к каждому предложению
|
||||
chainProposals.forEach(proposal => {
|
||||
proposal.chainId = network.chainId;
|
||||
proposal.contractAddress = network.address;
|
||||
proposal.networkName = getChainName(network.chainId);
|
||||
|
||||
// Детальная информация о каждом предложении
|
||||
rawProposals.forEach((proposal, index) => {
|
||||
console.log(`[Proposals] Предложение ${index}:`, {
|
||||
// Группируем предложения по описанию
|
||||
const key = `${proposal.description}_${proposal.initiator}`;
|
||||
if (!proposalsByDescription.has(key)) {
|
||||
proposalsByDescription.set(key, {
|
||||
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
|
||||
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);
|
||||
@@ -95,9 +158,9 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
||||
|
||||
proposals.value = activeProposals;
|
||||
filterProposals();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки предложений:', error);
|
||||
proposals.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
@@ -517,7 +580,106 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
||||
}
|
||||
};
|
||||
|
||||
// Мульти-чейн функции
|
||||
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 {
|
||||
// ... существующие поля
|
||||
proposals,
|
||||
filteredProposals,
|
||||
isLoading,
|
||||
@@ -529,15 +691,21 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
||||
loadProposals,
|
||||
filterProposals,
|
||||
voteOnProposal,
|
||||
voteOnMultichainProposal,
|
||||
executeProposal,
|
||||
executeMultichainProposal,
|
||||
cancelProposal,
|
||||
getProposalStatusClass,
|
||||
getProposalStatusText,
|
||||
getQuorumPercentage,
|
||||
getRequiredQuorumPercentage,
|
||||
canVote,
|
||||
canVoteMultichain,
|
||||
canExecute,
|
||||
canExecuteMultichain,
|
||||
canCancel,
|
||||
getChainStatusClass,
|
||||
getChainStatusText,
|
||||
updateProposalState,
|
||||
// Валидация
|
||||
validationStats,
|
||||
|
||||
@@ -278,6 +278,11 @@ const routes = [
|
||||
name: 'management-add-module',
|
||||
component: () => import('../views/smartcontracts/AddModuleFormView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/transfer-tokens',
|
||||
name: 'management-transfer-tokens',
|
||||
component: () => import('../views/smartcontracts/TransferTokensFormView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/modules',
|
||||
name: 'management-modules',
|
||||
|
||||
@@ -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 смарт-контракта
|
||||
* АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
|
||||
* Для обновления запустите: node backend/scripts/generate-abi.js
|
||||
*
|
||||
* Последнее обновление: 2025-09-29T18:16:32.027Z
|
||||
* Последнее обновление: 2025-12-29T12:09:15.558Z
|
||||
*/
|
||||
|
||||
export const DLE_ABI = [
|
||||
@@ -31,7 +19,7 @@ export const DLE_ABI = [
|
||||
"function checkpoints(address account, uint32 pos) returns (tuple)",
|
||||
"function clock() returns (uint48)",
|
||||
"function createAddModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) returns (uint256)",
|
||||
"function createProposal(string _description, uint256 _duration, bytes _operation, uint256 _governanceChainId, uint256[] _targetChains, uint256 ) returns (uint256)",
|
||||
"function 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 decimals() returns (uint8)",
|
||||
"function delegate(address delegatee)",
|
||||
|
||||
@@ -205,7 +205,6 @@ export async function createProposal(dleAddress, proposalData) {
|
||||
proposalData.description,
|
||||
proposalData.duration,
|
||||
proposalData.operation,
|
||||
proposalData.governanceChainId,
|
||||
proposalData.targetChains || [],
|
||||
proposalData.timelockDelay || 0
|
||||
);
|
||||
@@ -216,6 +215,7 @@ export async function createProposal(dleAddress, proposalData) {
|
||||
console.log('Предложение создано, tx hash:', tx.hash);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
proposalId: receipt.logs[0]?.topics[1] || '0', // Извлекаем ID предложения из события
|
||||
txHash: tx.hash,
|
||||
blockNumber: receipt.blockNumber
|
||||
|
||||
@@ -20,7 +20,7 @@ import { SiweMessage } from 'siwe';
|
||||
* Нормализует Ethereum адрес
|
||||
*/
|
||||
const normalizeAddress = (address) => {
|
||||
return ethers.getAddress ? ethers.getAddress(address) : ethers.utils.getAddress(address);
|
||||
return ethers.getAddress(address);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,25 +21,9 @@
|
||||
<div class="management-container">
|
||||
<!-- Деплоированные DLE -->
|
||||
<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">
|
||||
<p>Загрузка деплоированных DLE...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="deployedDles.length === 0" class="no-dles">
|
||||
<div v-if="deployedDles.length === 0" class="no-dles">
|
||||
<p>Деплоированных DLE пока нет</p>
|
||||
<p>Создайте новый DLE на странице <a href="/settings/dle-v2-deploy" class="link">Деплой DLE</a></p>
|
||||
</div>
|
||||
@@ -176,7 +160,6 @@ const router = useRouter();
|
||||
|
||||
// Состояние для DLE
|
||||
const deployedDles = ref([]);
|
||||
const isLoadingDles = ref(false);
|
||||
|
||||
|
||||
|
||||
@@ -228,7 +211,6 @@ const openSettings = () => {
|
||||
// Загрузка деплоированных DLE из блокчейна
|
||||
async function loadDeployedDles() {
|
||||
try {
|
||||
isLoadingDles.value = true;
|
||||
console.log('[ManagementView] Начинаем загрузку DLE...');
|
||||
|
||||
// Сначала получаем список DLE из API
|
||||
@@ -311,8 +293,6 @@ async function loadDeployedDles() {
|
||||
} catch (error) {
|
||||
console.error('[ManagementView] Ошибка при загрузке DLE:', error);
|
||||
deployedDles.value = [];
|
||||
} finally {
|
||||
isLoadingDles.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,74 +447,6 @@ onMounted(() => {
|
||||
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,
|
||||
.no-dles {
|
||||
|
||||
@@ -161,7 +161,7 @@ import { ref, computed, onMounted, onUnmounted, defineProps, defineEmits, inject
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAuthContext } from '../../composables/useAuth';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
|
||||
import { getDLEInfo } from '../../services/dleV2Service.js';
|
||||
import { createProposal as createProposalAPI } from '../../services/proposalsService.js';
|
||||
import { getModuleOperations } from '../../services/moduleOperationsService.js';
|
||||
import api from '../../api/axios';
|
||||
@@ -231,8 +231,11 @@ const isModulesWSConnected = ref(false);
|
||||
|
||||
// Функции для открытия отдельных форм операций
|
||||
function openTransferForm() {
|
||||
// TODO: Открыть форму для передачи токенов
|
||||
alert('Форма передачи токенов будет реализована');
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/transfer-tokens?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/transfer-tokens');
|
||||
}
|
||||
}
|
||||
|
||||
function openAddModuleForm() {
|
||||
@@ -321,9 +324,15 @@ async function loadDleData() {
|
||||
console.error('Ошибка загрузки DLE:', response.data.error);
|
||||
}
|
||||
|
||||
// Загружаем поддерживаемые цепочки
|
||||
const chainsResponse = await getSupportedChains(dleAddress.value);
|
||||
availableChains.value = chainsResponse.data?.chains || [];
|
||||
// Получаем поддерживаемые цепочки из данных DLE
|
||||
if (selectedDle.value?.deployedNetworks) {
|
||||
availableChains.value = selectedDle.value.deployedNetworks.map(net => ({
|
||||
chainId: net.chainId,
|
||||
name: getChainName(net.chainId)
|
||||
}));
|
||||
} else {
|
||||
availableChains.value = [];
|
||||
}
|
||||
|
||||
// Загружаем операции модулей
|
||||
await loadModuleOperations();
|
||||
@@ -497,6 +506,21 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
788
frontend/src/views/smartcontracts/TransferTokensFormView.vue
Normal file
788
frontend/src/views/smartcontracts/TransferTokensFormView.vue
Normal 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>
|
||||
@@ -2506,9 +2506,9 @@ prelude-ls@^1.2.1:
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz#6a31f88a4bad6c7adda253de12ba4edaea80ebcd"
|
||||
integrity sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user