diff --git a/backend/app.js b/backend/app.js index 3b5ef97..32c1230 100644 --- a/backend/app.js +++ b/backend/app.js @@ -92,11 +92,11 @@ const blockchainRoutes = require('./routes/blockchain'); // Добавляем const dleCoreRoutes = require('./routes/dleCore'); // Основные функции DLE const dleProposalsRoutes = require('./routes/dleProposals'); // Функции предложений const dleModulesRoutes = require('./routes/dleModules'); // Функции модулей +const dleMultichainRoutes = require('./routes/dleMultichain'); // Мультичейн функции const moduleDeploymentRoutes = require('./routes/moduleDeployment'); // Деплой модулей const dleTokensRoutes = require('./routes/dleTokens'); // Функции токенов const dleAnalyticsRoutes = require('./routes/dleAnalytics'); // Аналитика и история const compileRoutes = require('./routes/compile'); // Компиляция контрактов -const dleMultichainRoutes = require('./routes/dleMultichain'); // Мультичейн функции const { router: dleHistoryRoutes } = require('./routes/dleHistory'); // Расширенная история const systemRoutes = require('./routes/system'); // Добавляем импорт маршрутов системного мониторинга @@ -195,12 +195,13 @@ app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов app.use('/api/blockchain', blockchainRoutes); // Добавляем маршрут blockchain app.use('/api/dle-core', dleCoreRoutes); // Основные функции DLE +app.use('/api/dle-core', dleMultichainRoutes); // Мультичейн функции (используем тот же префикс) app.use('/api/dle-proposals', dleProposalsRoutes); // Функции предложений app.use('/api/dle-modules', dleModulesRoutes); // Функции модулей app.use('/api/module-deployment', moduleDeploymentRoutes); // Деплой модулей app.use('/api/dle-tokens', dleTokensRoutes); // Функции токенов app.use('/api/dle-analytics', dleAnalyticsRoutes); // Аналитика и история -app.use('/api/dle-multichain', dleMultichainRoutes); // Мультичейн функции +app.use('/api/dle-multichain-execution', require('./routes/dleMultichainExecution')); // Мультиконтрактное исполнение app.use('/api/dle-history', dleHistoryRoutes); // Расширенная история app.use('/api/messages', messagesRoutes); app.use('/api/identities', identitiesRoutes); diff --git a/backend/contracts/DLE.sol b/backend/contracts/DLE.sol index 743793a..763c265 100644 --- a/backend/contracts/DLE.sol +++ b/backend/contracts/DLE.sol @@ -1,13 +1,7 @@ // SPDX-License-Identifier: PROPRIETARY AND MIT // Copyright (c) 2024-2025 Тарабанов Александр Викторович // All rights reserved. -// -// This software is proprietary and confidential. -// Unauthorized copying, modification, or distribution is prohibited. -// // For licensing inquiries: info@hb3-accelerator.com -// Website: https://hb3-accelerator.com -// GitHub: https://github.com/HB3-ACCELERATOR pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -21,36 +15,12 @@ interface IERC1271 { function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); } -/** - * @dev Интерфейс для мультичейн метаданных (EIP-3668 inspired) - */ interface IMultichainMetadata { - /** - * @dev Возвращает информацию о мультичейн развертывании - * @return supportedChainIds Массив всех поддерживаемых chain ID - * @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых) - */ function getMultichainInfo() external view returns (uint256[] memory supportedChainIds, uint256 defaultVotingChain); - - /** - * @dev Возвращает адреса контракта в других сетях - * @return chainIds Массив chain ID где развернут контракт - * @return addresses Массив адресов контракта в соответствующих сетях - */ function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses); } -/** - * @title DLE (Digital Legal Entity) - * @dev Основной контракт DLE с модульной архитектурой, Single-Chain Governance - * и безопасной мульти-чейн синхронизацией без сторонних мостов (через подписи холдеров). - * - * КЛЮЧЕВЫЕ ОСОБЕННОСТИ: - * - Прямые переводы токенов ЗАБЛОКИРОВАНЫ (transfer, transferFrom, approve) - * - Перевод токенов возможен ТОЛЬКО через governance предложения - * - Токены служат только для голосования и управления DLE - * - Все операции с токенами требуют коллективного решения - */ +// DLE (Digital Legal Entity) - основной контракт с модульной архитектурой contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMetadata { using ECDSA for bytes32; struct DLEInfo { @@ -101,7 +71,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta DLEInfo public dleInfo; uint256 public quorumPercentage; uint256 public proposalCounter; - uint256 public currentChainId; + // Удален currentChainId - теперь используется block.chainid для проверок // Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор) string public logoURI; @@ -169,6 +139,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta error ErrProposalExecuted(); error ErrAlreadyVoted(); error ErrWrongChain(); + error ErrUnsupportedChain(); error ErrNoPower(); error ErrNotReady(); error ErrNotInitiator(); @@ -200,7 +171,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta constructor( DLEConfig memory config, - uint256 _currentChainId, address _initializer ) ERC20(config.name, config.symbol) ERC20Permit(config.name) { if (_initializer == address(0)) revert ErrZeroAddress(); @@ -218,7 +188,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta }); quorumPercentage = config.quorumPercentage; - currentChainId = _currentChainId; // Настраиваем поддерживаемые цепочки for (uint256 i = 0; i < config.supportedChainIds.length; i++) { @@ -254,9 +223,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta ); } - /** - * @dev Одноразовая инициализация URI логотипа. Доступно только инициализатору и только один раз. - */ + // Одноразовая инициализация URI логотипа function initializeLogoURI(string calldata _logoURI) external { if (msg.sender != initializer) revert ErrOnlyInitializer(); if (bytes(logoURI).length != 0) revert ErrLogoAlreadySet(); @@ -265,13 +232,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta emit LogoURIUpdated(old, _logoURI); } - /** - * @dev Создать предложение с выбором цепочки для кворума - * @param _description Описание предложения - * @param _duration Длительность голосования в секундах - * @param _operation Операция для исполнения - * @param _governanceChainId ID цепочки для сбора голосов - */ + // Создать предложение с выбором цепочки для кворума function createProposal( string memory _description, uint256 _duration, @@ -333,11 +294,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta return proposalId; } - /** - * @dev Голосовать за предложение - * @param _proposalId ID предложения - * @param _support Поддержка предложения - */ + // Голосовать за предложение function vote(uint256 _proposalId, bool _support) external nonReentrant { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); @@ -345,7 +302,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta if (proposal.executed) revert ErrProposalExecuted(); if (proposal.canceled) revert ErrProposalCanceled(); if (proposal.hasVoted[msg.sender]) revert ErrAlreadyVoted(); - if (currentChainId != proposal.governanceChainId) revert ErrWrongChain(); + // Проверяем, что текущая сеть поддерживается + if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint); if (votingPower == 0) revert ErrNoPower(); @@ -360,7 +318,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta emit ProposalVoted(_proposalId, msg.sender, _support, votingPower); } - // УДАЛЕНО: syncVoteFromChain с MerkleProof — небезопасно без доверенного моста function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) { Proposal storage proposal = proposals[_proposalId]; if (proposal.id != _proposalId) revert ErrProposalMissing(); @@ -382,7 +339,8 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta if (proposal.id != _proposalId) revert ErrProposalMissing(); if (proposal.executed) revert ErrProposalExecuted(); if (proposal.canceled) revert ErrProposalCanceled(); - if (currentChainId != proposal.governanceChainId) revert ErrWrongChain(); + // Проверяем, что текущая сеть поддерживается + if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); (bool passed, bool quorumReached) = checkProposalResult(_proposalId); @@ -424,8 +382,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta if (proposal.id != _proposalId) revert ErrProposalMissing(); if (proposal.executed) revert ErrProposalExecuted(); if (proposal.canceled) revert ErrProposalCanceled(); - if (currentChainId == proposal.governanceChainId) revert ErrWrongChain(); - if (!_isTargetChain(proposal, currentChainId)) revert ErrBadTarget(); + // Проверяем, что текущая сеть поддерживается + if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); + // Проверяем, что текущая сеть является целевой для предложения + if (!_isTargetChain(proposal, block.chainid)) revert ErrBadTarget(); if (signers.length != signatures.length) revert ErrSigLengthMismatch(); if (signers.length == 0) revert ErrNoSigners(); @@ -436,7 +396,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta EXECUTION_APPROVAL_TYPEHASH, _proposalId, opHash, - currentChainId, + block.chainid, proposal.snapshotTimepoint )); bytes32 digest = _hashTypedDataV4(structHash); @@ -474,14 +434,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta proposal.executed = true; _executeOperation(proposal.operation); emit ProposalExecuted(_proposalId, proposal.operation); - emit ProposalExecutionApprovedInChain(_proposalId, currentChainId); + emit ProposalExecutionApprovedInChain(_proposalId, block.chainid); } - // Sync функции удалены для экономии байт-кода - - // УДАЛЕНО: syncToChain — не используется в подпись‑ориентированной схеме - /** * @dev Получить количество поддерживаемых цепочек */ @@ -505,7 +461,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta // Управление списком сетей теперь выполняется только через предложения function _addSupportedChain(uint256 _chainId) internal { require(!supportedChains[_chainId], "Chain already supported"); - require(_chainId != currentChainId, "Cannot add current chain"); + require(_chainId != block.chainid, "Cannot add current chain"); supportedChains[_chainId] = true; supportedChainIds.push(_chainId); emit ChainAdded(_chainId); @@ -517,7 +473,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta */ function _removeSupportedChain(uint256 _chainId) internal { require(supportedChains[_chainId], "Chain not supported"); - require(_chainId != currentChainId, "Cannot remove current chain"); + require(_chainId != block.chainid, "Cannot remove current chain"); supportedChains[_chainId] = false; // Удаляем из массива for (uint256 i = 0; i < supportedChainIds.length; i++) { @@ -530,18 +486,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta emit ChainRemoved(_chainId); } - /** - * @dev Установить Merkle root для цепочки (только для владельцев токенов) - * @param _chainId ID цепочки - * @param _merkleRoot Merkle root для цепочки - */ - // УДАЛЕНО: setChainMerkleRoot — небезопасно отдавать любому холдеру - - /** - * @dev Получить Merkle root для цепочки - * @param _chainId ID цепочки - */ - // УДАЛЕНО: getChainMerkleRoot — устарело /** * @dev Исполнить операцию @@ -856,10 +800,10 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta } /** - * @dev Получить текущий ID цепочки + * @dev Получить текущий ID цепочки (теперь используется block.chainid) */ function getCurrentChainId() external view returns (uint256) { - return currentChainId; + return block.chainid; } /** @@ -884,7 +828,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta * @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых) */ function getMultichainInfo() external view returns (uint256[] memory chains, uint256 defaultVotingChain) { - return (supportedChainIds, currentChainId); + return (supportedChainIds, block.chainid); } /** @@ -898,7 +842,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta for (uint256 i = 0; i < supportedChainIds.length; i++) { chains[i] = supportedChainIds[i]; - addrs[i] = address(this); // CREATE2 обеспечивает одинаковые адреса + addrs[i] = address(this); // Детерминированный деплой обеспечивает одинаковые адреса } return (chains, addrs); @@ -929,7 +873,7 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta json, '],', '"defaultVotingChain": ', - _toString(currentChainId), + _toString(block.chainid), ',', '"note": "All chains are equal, voting can happen on any supported chain",', '"contractAddress": "', @@ -985,26 +929,6 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta return string(str); } - /** - * @dev Получить информацию об архитектуре мультичейн governance - * @return architecture Описание архитектуры в JSON формате - */ - function getGovernanceArchitecture() external pure returns (string memory architecture) { - return string(abi.encodePacked( - '{"architecture": {', - '"type": "Single-Chain Governance",', - '"description": "Voting happens on one chain per proposal, execution on any supported chain",', - '"features": [', - '"Equal chain support - no primary chain",', - '"Cross-chain execution via signatures",', - '"Deterministic addresses via CREATE2",', - '"No bridge dependencies"', - '],', - '"voting": "One chain per proposal (chosen by proposer)",', - '"execution": "Any supported chain via signature verification"', - '}}' - )); - } // API функции вынесены в отдельный reader контракт для экономии байт-кода @@ -1025,6 +949,38 @@ contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMeta // Функции для подсчёта голосов вынесены в reader контракт + // Получить полную сводку по предложению + function getProposalSummary(uint256 _proposalId) external view returns ( + uint256 id, + string memory description, + uint256 forVotes, + uint256 againstVotes, + bool executed, + bool canceled, + uint256 deadline, + address initiator, + uint256 governanceChainId, + uint256 snapshotTimepoint, + uint256[] memory targetChains + ) { + Proposal storage p = proposals[_proposalId]; + require(p.id == _proposalId, "Proposal does not exist"); + + return ( + p.id, + p.description, + p.forVotes, + p.againstVotes, + p.executed, + p.canceled, + p.deadline, + p.initiator, + p.governanceChainId, + p.snapshotTimepoint, + p.targetChains + ); + } + // Деактивация вынесена в отдельный модуль. См. DeactivationModule. function isActive() external view returns (bool) { return dleInfo.isActive; diff --git a/backend/contracts/DLE_flattened.sol b/backend/contracts/DLE_flattened.sol new file mode 100644 index 0000000..5c480e1 --- /dev/null +++ b/backend/contracts/DLE_flattened.sol @@ -0,0 +1,6357 @@ +// Sources flattened with hardhat v2.26.1 https://hardhat.org + +// SPDX-License-Identifier: MIT AND PROPRIETARY + +// File @openzeppelin/contracts/governance/utils/IVotes.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (governance/utils/IVotes.sol) +pragma solidity ^0.8.20; + +/** + * @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. + */ +interface IVotes { + /** + * @dev The signature used has expired. + */ + error VotesExpiredSignature(uint256 expiry); + + /** + * @dev Emitted when an account changes their delegate. + */ + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + + /** + * @dev Emitted when a token transfer or delegate change results in changes to a delegate's number of voting units. + */ + event DelegateVotesChanged(address indexed delegate, uint256 previousVotes, uint256 newVotes); + + /** + * @dev Returns the current amount of votes that `account` has. + */ + function getVotes(address account) external view returns (uint256); + + /** + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + */ + function getPastVotes(address account, uint256 timepoint) external view returns (uint256); + + /** + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + * + * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. + * Votes that have not been delegated are still part of total supply, even though they would not participate in a + * vote. + */ + function getPastTotalSupply(uint256 timepoint) external view returns (uint256); + + /** + * @dev Returns the delegate that `account` has chosen. + */ + function delegates(address account) external view returns (address); + + /** + * @dev Delegates votes from the sender to `delegatee`. + */ + function delegate(address delegatee) external; + + /** + * @dev Delegates votes from signer to `delegatee`. + */ + function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external; +} + + +// File @openzeppelin/contracts/interfaces/IERC6372.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC6372.sol) + +pragma solidity ^0.8.20; + +interface IERC6372 { + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting). + */ + function clock() external view returns (uint48); + + /** + * @dev Description of the clock + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() external view returns (string memory); +} + + +// File @openzeppelin/contracts/interfaces/IERC5805.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5805.sol) + +pragma solidity ^0.8.20; + + +interface IERC5805 is IERC6372, IVotes {} + + +// File @openzeppelin/contracts/utils/Context.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Provides information about the current execution context, including the + * sender of the transaction and its data. While these are generally available + * via msg.sender and msg.data, they should not be accessed in such a direct + * manner, since when dealing with meta-transactions the account sending and + * paying for execution may not be the actual sender (as far as an application + * is concerned). + * + * This contract is only required for intermediate, library-like contracts. + */ +abstract contract Context { + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + + function _contextSuffixLength() internal view virtual returns (uint256) { + return 0; + } +} + + +// File @openzeppelin/contracts/utils/cryptography/ECDSA.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + */ +library ECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + */ + function tryRecover( + bytes32 hash, + bytes memory signature + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + assembly ("memory-safe") { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures] + */ + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} + + +// File @openzeppelin/contracts/interfaces/IERC5267.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC5267.sol) + +pragma solidity ^0.8.20; + +interface IERC5267 { + /** + * @dev MAY be emitted to signal that the domain could have changed. + */ + event EIP712DomainChanged(); + + /** + * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712 + * signature. + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); +} + + +// File @openzeppelin/contracts/utils/math/SafeCast.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol) +// This file was procedurally generated from scripts/generate/templates/SafeCast.js. + +pragma solidity ^0.8.20; + +/** + * @dev Wrappers over Solidity's uintXX/intXX/bool casting operators with added overflow + * checks. + * + * Downcasting from uint256/int256 in Solidity does not revert on overflow. This can + * easily result in undesired exploitation or bugs, since developers usually + * assume that overflows raise errors. `SafeCast` restores this intuition by + * reverting the transaction when such an operation overflows. + * + * Using this library instead of the unchecked operations eliminates an entire + * class of bugs, so it's recommended to use it always. + */ +library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + + /** + * @dev Returns the downcasted uint248 from uint256, reverting on + * overflow (when the input is greater than largest uint248). + * + * Counterpart to Solidity's `uint248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toUint248(uint256 value) internal pure returns (uint248) { + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } + return uint248(value); + } + + /** + * @dev Returns the downcasted uint240 from uint256, reverting on + * overflow (when the input is greater than largest uint240). + * + * Counterpart to Solidity's `uint240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toUint240(uint256 value) internal pure returns (uint240) { + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } + return uint240(value); + } + + /** + * @dev Returns the downcasted uint232 from uint256, reverting on + * overflow (when the input is greater than largest uint232). + * + * Counterpart to Solidity's `uint232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toUint232(uint256 value) internal pure returns (uint232) { + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } + return uint232(value); + } + + /** + * @dev Returns the downcasted uint224 from uint256, reverting on + * overflow (when the input is greater than largest uint224). + * + * Counterpart to Solidity's `uint224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toUint224(uint256 value) internal pure returns (uint224) { + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } + return uint224(value); + } + + /** + * @dev Returns the downcasted uint216 from uint256, reverting on + * overflow (when the input is greater than largest uint216). + * + * Counterpart to Solidity's `uint216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toUint216(uint256 value) internal pure returns (uint216) { + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } + return uint216(value); + } + + /** + * @dev Returns the downcasted uint208 from uint256, reverting on + * overflow (when the input is greater than largest uint208). + * + * Counterpart to Solidity's `uint208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toUint208(uint256 value) internal pure returns (uint208) { + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } + return uint208(value); + } + + /** + * @dev Returns the downcasted uint200 from uint256, reverting on + * overflow (when the input is greater than largest uint200). + * + * Counterpart to Solidity's `uint200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toUint200(uint256 value) internal pure returns (uint200) { + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } + return uint200(value); + } + + /** + * @dev Returns the downcasted uint192 from uint256, reverting on + * overflow (when the input is greater than largest uint192). + * + * Counterpart to Solidity's `uint192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toUint192(uint256 value) internal pure returns (uint192) { + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } + return uint192(value); + } + + /** + * @dev Returns the downcasted uint184 from uint256, reverting on + * overflow (when the input is greater than largest uint184). + * + * Counterpart to Solidity's `uint184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toUint184(uint256 value) internal pure returns (uint184) { + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } + return uint184(value); + } + + /** + * @dev Returns the downcasted uint176 from uint256, reverting on + * overflow (when the input is greater than largest uint176). + * + * Counterpart to Solidity's `uint176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toUint176(uint256 value) internal pure returns (uint176) { + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } + return uint176(value); + } + + /** + * @dev Returns the downcasted uint168 from uint256, reverting on + * overflow (when the input is greater than largest uint168). + * + * Counterpart to Solidity's `uint168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toUint168(uint256 value) internal pure returns (uint168) { + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } + return uint168(value); + } + + /** + * @dev Returns the downcasted uint160 from uint256, reverting on + * overflow (when the input is greater than largest uint160). + * + * Counterpart to Solidity's `uint160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toUint160(uint256 value) internal pure returns (uint160) { + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } + return uint160(value); + } + + /** + * @dev Returns the downcasted uint152 from uint256, reverting on + * overflow (when the input is greater than largest uint152). + * + * Counterpart to Solidity's `uint152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toUint152(uint256 value) internal pure returns (uint152) { + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } + return uint152(value); + } + + /** + * @dev Returns the downcasted uint144 from uint256, reverting on + * overflow (when the input is greater than largest uint144). + * + * Counterpart to Solidity's `uint144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toUint144(uint256 value) internal pure returns (uint144) { + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } + return uint144(value); + } + + /** + * @dev Returns the downcasted uint136 from uint256, reverting on + * overflow (when the input is greater than largest uint136). + * + * Counterpart to Solidity's `uint136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toUint136(uint256 value) internal pure returns (uint136) { + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } + return uint136(value); + } + + /** + * @dev Returns the downcasted uint128 from uint256, reverting on + * overflow (when the input is greater than largest uint128). + * + * Counterpart to Solidity's `uint128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toUint128(uint256 value) internal pure returns (uint128) { + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } + return uint128(value); + } + + /** + * @dev Returns the downcasted uint120 from uint256, reverting on + * overflow (when the input is greater than largest uint120). + * + * Counterpart to Solidity's `uint120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toUint120(uint256 value) internal pure returns (uint120) { + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } + return uint120(value); + } + + /** + * @dev Returns the downcasted uint112 from uint256, reverting on + * overflow (when the input is greater than largest uint112). + * + * Counterpart to Solidity's `uint112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toUint112(uint256 value) internal pure returns (uint112) { + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } + return uint112(value); + } + + /** + * @dev Returns the downcasted uint104 from uint256, reverting on + * overflow (when the input is greater than largest uint104). + * + * Counterpart to Solidity's `uint104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toUint104(uint256 value) internal pure returns (uint104) { + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } + return uint104(value); + } + + /** + * @dev Returns the downcasted uint96 from uint256, reverting on + * overflow (when the input is greater than largest uint96). + * + * Counterpart to Solidity's `uint96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toUint96(uint256 value) internal pure returns (uint96) { + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } + return uint96(value); + } + + /** + * @dev Returns the downcasted uint88 from uint256, reverting on + * overflow (when the input is greater than largest uint88). + * + * Counterpart to Solidity's `uint88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toUint88(uint256 value) internal pure returns (uint88) { + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } + return uint88(value); + } + + /** + * @dev Returns the downcasted uint80 from uint256, reverting on + * overflow (when the input is greater than largest uint80). + * + * Counterpart to Solidity's `uint80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toUint80(uint256 value) internal pure returns (uint80) { + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } + return uint80(value); + } + + /** + * @dev Returns the downcasted uint72 from uint256, reverting on + * overflow (when the input is greater than largest uint72). + * + * Counterpart to Solidity's `uint72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toUint72(uint256 value) internal pure returns (uint72) { + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } + return uint72(value); + } + + /** + * @dev Returns the downcasted uint64 from uint256, reverting on + * overflow (when the input is greater than largest uint64). + * + * Counterpart to Solidity's `uint64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toUint64(uint256 value) internal pure returns (uint64) { + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } + return uint64(value); + } + + /** + * @dev Returns the downcasted uint56 from uint256, reverting on + * overflow (when the input is greater than largest uint56). + * + * Counterpart to Solidity's `uint56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toUint56(uint256 value) internal pure returns (uint56) { + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } + return uint56(value); + } + + /** + * @dev Returns the downcasted uint48 from uint256, reverting on + * overflow (when the input is greater than largest uint48). + * + * Counterpart to Solidity's `uint48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toUint48(uint256 value) internal pure returns (uint48) { + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } + return uint48(value); + } + + /** + * @dev Returns the downcasted uint40 from uint256, reverting on + * overflow (when the input is greater than largest uint40). + * + * Counterpart to Solidity's `uint40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toUint40(uint256 value) internal pure returns (uint40) { + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } + return uint40(value); + } + + /** + * @dev Returns the downcasted uint32 from uint256, reverting on + * overflow (when the input is greater than largest uint32). + * + * Counterpart to Solidity's `uint32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toUint32(uint256 value) internal pure returns (uint32) { + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } + return uint32(value); + } + + /** + * @dev Returns the downcasted uint24 from uint256, reverting on + * overflow (when the input is greater than largest uint24). + * + * Counterpart to Solidity's `uint24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toUint24(uint256 value) internal pure returns (uint24) { + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } + return uint24(value); + } + + /** + * @dev Returns the downcasted uint16 from uint256, reverting on + * overflow (when the input is greater than largest uint16). + * + * Counterpart to Solidity's `uint16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toUint16(uint256 value) internal pure returns (uint16) { + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } + return uint16(value); + } + + /** + * @dev Returns the downcasted uint8 from uint256, reverting on + * overflow (when the input is greater than largest uint8). + * + * Counterpart to Solidity's `uint8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toUint8(uint256 value) internal pure returns (uint8) { + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } + return uint8(value); + } + + /** + * @dev Converts a signed int256 into an unsigned uint256. + * + * Requirements: + * + * - input must be greater than or equal to 0. + */ + function toUint256(int256 value) internal pure returns (uint256) { + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } + return uint256(value); + } + + /** + * @dev Returns the downcasted int248 from int256, reverting on + * overflow (when the input is less than smallest int248 or + * greater than largest int248). + * + * Counterpart to Solidity's `int248` operator. + * + * Requirements: + * + * - input must fit into 248 bits + */ + function toInt248(int256 value) internal pure returns (int248 downcasted) { + downcasted = int248(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } + } + + /** + * @dev Returns the downcasted int240 from int256, reverting on + * overflow (when the input is less than smallest int240 or + * greater than largest int240). + * + * Counterpart to Solidity's `int240` operator. + * + * Requirements: + * + * - input must fit into 240 bits + */ + function toInt240(int256 value) internal pure returns (int240 downcasted) { + downcasted = int240(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } + } + + /** + * @dev Returns the downcasted int232 from int256, reverting on + * overflow (when the input is less than smallest int232 or + * greater than largest int232). + * + * Counterpart to Solidity's `int232` operator. + * + * Requirements: + * + * - input must fit into 232 bits + */ + function toInt232(int256 value) internal pure returns (int232 downcasted) { + downcasted = int232(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } + } + + /** + * @dev Returns the downcasted int224 from int256, reverting on + * overflow (when the input is less than smallest int224 or + * greater than largest int224). + * + * Counterpart to Solidity's `int224` operator. + * + * Requirements: + * + * - input must fit into 224 bits + */ + function toInt224(int256 value) internal pure returns (int224 downcasted) { + downcasted = int224(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } + } + + /** + * @dev Returns the downcasted int216 from int256, reverting on + * overflow (when the input is less than smallest int216 or + * greater than largest int216). + * + * Counterpart to Solidity's `int216` operator. + * + * Requirements: + * + * - input must fit into 216 bits + */ + function toInt216(int256 value) internal pure returns (int216 downcasted) { + downcasted = int216(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } + } + + /** + * @dev Returns the downcasted int208 from int256, reverting on + * overflow (when the input is less than smallest int208 or + * greater than largest int208). + * + * Counterpart to Solidity's `int208` operator. + * + * Requirements: + * + * - input must fit into 208 bits + */ + function toInt208(int256 value) internal pure returns (int208 downcasted) { + downcasted = int208(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } + } + + /** + * @dev Returns the downcasted int200 from int256, reverting on + * overflow (when the input is less than smallest int200 or + * greater than largest int200). + * + * Counterpart to Solidity's `int200` operator. + * + * Requirements: + * + * - input must fit into 200 bits + */ + function toInt200(int256 value) internal pure returns (int200 downcasted) { + downcasted = int200(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } + } + + /** + * @dev Returns the downcasted int192 from int256, reverting on + * overflow (when the input is less than smallest int192 or + * greater than largest int192). + * + * Counterpart to Solidity's `int192` operator. + * + * Requirements: + * + * - input must fit into 192 bits + */ + function toInt192(int256 value) internal pure returns (int192 downcasted) { + downcasted = int192(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } + } + + /** + * @dev Returns the downcasted int184 from int256, reverting on + * overflow (when the input is less than smallest int184 or + * greater than largest int184). + * + * Counterpart to Solidity's `int184` operator. + * + * Requirements: + * + * - input must fit into 184 bits + */ + function toInt184(int256 value) internal pure returns (int184 downcasted) { + downcasted = int184(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } + } + + /** + * @dev Returns the downcasted int176 from int256, reverting on + * overflow (when the input is less than smallest int176 or + * greater than largest int176). + * + * Counterpart to Solidity's `int176` operator. + * + * Requirements: + * + * - input must fit into 176 bits + */ + function toInt176(int256 value) internal pure returns (int176 downcasted) { + downcasted = int176(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } + } + + /** + * @dev Returns the downcasted int168 from int256, reverting on + * overflow (when the input is less than smallest int168 or + * greater than largest int168). + * + * Counterpart to Solidity's `int168` operator. + * + * Requirements: + * + * - input must fit into 168 bits + */ + function toInt168(int256 value) internal pure returns (int168 downcasted) { + downcasted = int168(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } + } + + /** + * @dev Returns the downcasted int160 from int256, reverting on + * overflow (when the input is less than smallest int160 or + * greater than largest int160). + * + * Counterpart to Solidity's `int160` operator. + * + * Requirements: + * + * - input must fit into 160 bits + */ + function toInt160(int256 value) internal pure returns (int160 downcasted) { + downcasted = int160(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } + } + + /** + * @dev Returns the downcasted int152 from int256, reverting on + * overflow (when the input is less than smallest int152 or + * greater than largest int152). + * + * Counterpart to Solidity's `int152` operator. + * + * Requirements: + * + * - input must fit into 152 bits + */ + function toInt152(int256 value) internal pure returns (int152 downcasted) { + downcasted = int152(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } + } + + /** + * @dev Returns the downcasted int144 from int256, reverting on + * overflow (when the input is less than smallest int144 or + * greater than largest int144). + * + * Counterpart to Solidity's `int144` operator. + * + * Requirements: + * + * - input must fit into 144 bits + */ + function toInt144(int256 value) internal pure returns (int144 downcasted) { + downcasted = int144(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } + } + + /** + * @dev Returns the downcasted int136 from int256, reverting on + * overflow (when the input is less than smallest int136 or + * greater than largest int136). + * + * Counterpart to Solidity's `int136` operator. + * + * Requirements: + * + * - input must fit into 136 bits + */ + function toInt136(int256 value) internal pure returns (int136 downcasted) { + downcasted = int136(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } + } + + /** + * @dev Returns the downcasted int128 from int256, reverting on + * overflow (when the input is less than smallest int128 or + * greater than largest int128). + * + * Counterpart to Solidity's `int128` operator. + * + * Requirements: + * + * - input must fit into 128 bits + */ + function toInt128(int256 value) internal pure returns (int128 downcasted) { + downcasted = int128(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } + } + + /** + * @dev Returns the downcasted int120 from int256, reverting on + * overflow (when the input is less than smallest int120 or + * greater than largest int120). + * + * Counterpart to Solidity's `int120` operator. + * + * Requirements: + * + * - input must fit into 120 bits + */ + function toInt120(int256 value) internal pure returns (int120 downcasted) { + downcasted = int120(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } + } + + /** + * @dev Returns the downcasted int112 from int256, reverting on + * overflow (when the input is less than smallest int112 or + * greater than largest int112). + * + * Counterpart to Solidity's `int112` operator. + * + * Requirements: + * + * - input must fit into 112 bits + */ + function toInt112(int256 value) internal pure returns (int112 downcasted) { + downcasted = int112(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } + } + + /** + * @dev Returns the downcasted int104 from int256, reverting on + * overflow (when the input is less than smallest int104 or + * greater than largest int104). + * + * Counterpart to Solidity's `int104` operator. + * + * Requirements: + * + * - input must fit into 104 bits + */ + function toInt104(int256 value) internal pure returns (int104 downcasted) { + downcasted = int104(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } + } + + /** + * @dev Returns the downcasted int96 from int256, reverting on + * overflow (when the input is less than smallest int96 or + * greater than largest int96). + * + * Counterpart to Solidity's `int96` operator. + * + * Requirements: + * + * - input must fit into 96 bits + */ + function toInt96(int256 value) internal pure returns (int96 downcasted) { + downcasted = int96(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } + } + + /** + * @dev Returns the downcasted int88 from int256, reverting on + * overflow (when the input is less than smallest int88 or + * greater than largest int88). + * + * Counterpart to Solidity's `int88` operator. + * + * Requirements: + * + * - input must fit into 88 bits + */ + function toInt88(int256 value) internal pure returns (int88 downcasted) { + downcasted = int88(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } + } + + /** + * @dev Returns the downcasted int80 from int256, reverting on + * overflow (when the input is less than smallest int80 or + * greater than largest int80). + * + * Counterpart to Solidity's `int80` operator. + * + * Requirements: + * + * - input must fit into 80 bits + */ + function toInt80(int256 value) internal pure returns (int80 downcasted) { + downcasted = int80(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } + } + + /** + * @dev Returns the downcasted int72 from int256, reverting on + * overflow (when the input is less than smallest int72 or + * greater than largest int72). + * + * Counterpart to Solidity's `int72` operator. + * + * Requirements: + * + * - input must fit into 72 bits + */ + function toInt72(int256 value) internal pure returns (int72 downcasted) { + downcasted = int72(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } + } + + /** + * @dev Returns the downcasted int64 from int256, reverting on + * overflow (when the input is less than smallest int64 or + * greater than largest int64). + * + * Counterpart to Solidity's `int64` operator. + * + * Requirements: + * + * - input must fit into 64 bits + */ + function toInt64(int256 value) internal pure returns (int64 downcasted) { + downcasted = int64(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } + } + + /** + * @dev Returns the downcasted int56 from int256, reverting on + * overflow (when the input is less than smallest int56 or + * greater than largest int56). + * + * Counterpart to Solidity's `int56` operator. + * + * Requirements: + * + * - input must fit into 56 bits + */ + function toInt56(int256 value) internal pure returns (int56 downcasted) { + downcasted = int56(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } + } + + /** + * @dev Returns the downcasted int48 from int256, reverting on + * overflow (when the input is less than smallest int48 or + * greater than largest int48). + * + * Counterpart to Solidity's `int48` operator. + * + * Requirements: + * + * - input must fit into 48 bits + */ + function toInt48(int256 value) internal pure returns (int48 downcasted) { + downcasted = int48(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } + } + + /** + * @dev Returns the downcasted int40 from int256, reverting on + * overflow (when the input is less than smallest int40 or + * greater than largest int40). + * + * Counterpart to Solidity's `int40` operator. + * + * Requirements: + * + * - input must fit into 40 bits + */ + function toInt40(int256 value) internal pure returns (int40 downcasted) { + downcasted = int40(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } + } + + /** + * @dev Returns the downcasted int32 from int256, reverting on + * overflow (when the input is less than smallest int32 or + * greater than largest int32). + * + * Counterpart to Solidity's `int32` operator. + * + * Requirements: + * + * - input must fit into 32 bits + */ + function toInt32(int256 value) internal pure returns (int32 downcasted) { + downcasted = int32(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } + } + + /** + * @dev Returns the downcasted int24 from int256, reverting on + * overflow (when the input is less than smallest int24 or + * greater than largest int24). + * + * Counterpart to Solidity's `int24` operator. + * + * Requirements: + * + * - input must fit into 24 bits + */ + function toInt24(int256 value) internal pure returns (int24 downcasted) { + downcasted = int24(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } + } + + /** + * @dev Returns the downcasted int16 from int256, reverting on + * overflow (when the input is less than smallest int16 or + * greater than largest int16). + * + * Counterpart to Solidity's `int16` operator. + * + * Requirements: + * + * - input must fit into 16 bits + */ + function toInt16(int256 value) internal pure returns (int16 downcasted) { + downcasted = int16(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } + } + + /** + * @dev Returns the downcasted int8 from int256, reverting on + * overflow (when the input is less than smallest int8 or + * greater than largest int8). + * + * Counterpart to Solidity's `int8` operator. + * + * Requirements: + * + * - input must fit into 8 bits + */ + function toInt8(int256 value) internal pure returns (int8 downcasted) { + downcasted = int8(value); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } + } + + /** + * @dev Converts an unsigned uint256 into a signed int256. + * + * Requirements: + * + * - input must be less than or equal to maxInt256. + */ + function toInt256(uint256 value) internal pure returns (int256) { + // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } + return int256(value); + } + + /** + * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump. + */ + function toUint(bool b) internal pure returns (uint256 u) { + assembly ("memory-safe") { + u := iszero(iszero(b)) + } + } +} + + +// File @openzeppelin/contracts/utils/Panic.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Helper library for emitting standardized panic codes. + * + * ```solidity + * contract Example { + * using Panic for uint256; + * + * // Use any of the declared internal constants + * function foo() { Panic.GENERIC.panic(); } + * + * // Alternatively + * function foo() { Panic.panic(Panic.GENERIC); } + * } + * ``` + * + * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil]. + * + * _Available since v5.1._ + */ +// slither-disable-next-line unused-state +library Panic { + /// @dev generic / unspecified error + uint256 internal constant GENERIC = 0x00; + /// @dev used by the assert() builtin + uint256 internal constant ASSERT = 0x01; + /// @dev arithmetic underflow or overflow + uint256 internal constant UNDER_OVERFLOW = 0x11; + /// @dev division or modulo by zero + uint256 internal constant DIVISION_BY_ZERO = 0x12; + /// @dev enum conversion error + uint256 internal constant ENUM_CONVERSION_ERROR = 0x21; + /// @dev invalid encoding in storage + uint256 internal constant STORAGE_ENCODING_ERROR = 0x22; + /// @dev empty array pop + uint256 internal constant EMPTY_ARRAY_POP = 0x31; + /// @dev array out of bounds access + uint256 internal constant ARRAY_OUT_OF_BOUNDS = 0x32; + /// @dev resource error (too large allocation or too large array) + uint256 internal constant RESOURCE_ERROR = 0x41; + /// @dev calling invalid internal function + uint256 internal constant INVALID_INTERNAL_FUNCTION = 0x51; + + /// @dev Reverts with a panic code. Recommended to use with + /// the internal constants with predefined codes. + function panic(uint256 code) internal pure { + assembly ("memory-safe") { + mstore(0x00, 0x4e487b71) + mstore(0x20, code) + revert(0x1c, 0x24) + } + } +} + + +// File @openzeppelin/contracts/utils/math/Math.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/Math.sol) + +pragma solidity ^0.8.20; + + +/** + * @dev Standard math utilities missing in the Solidity language. + */ +library Math { + enum Rounding { + Floor, // Toward negative infinity + Ceil, // Toward positive infinity + Trunc, // Toward zero + Expand // Away from zero + } + + /** + * @dev Returns the addition of two unsigned integers, with an success flag (no overflow). + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an success flag (no overflow). + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an success flag (no overflow). + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a success flag (no division by zero). + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a success flag (no division by zero). + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + + /** + * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant. + * + * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone. + * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute + * one branch when needed, making this function more expensive. + */ + function ternary(bool condition, uint256 a, uint256 b) internal pure returns (uint256) { + unchecked { + // branchless ternary works because: + // b ^ (a ^ b) == a + // b ^ 0 == b + return b ^ ((a ^ b) * SafeCast.toUint(condition)); + } + } + + /** + * @dev Returns the largest of two numbers. + */ + function max(uint256 a, uint256 b) internal pure returns (uint256) { + return ternary(a > b, a, b); + } + + /** + * @dev Returns the smallest of two numbers. + */ + function min(uint256 a, uint256 b) internal pure returns (uint256) { + return ternary(a < b, a, b); + } + + /** + * @dev Returns the average of two numbers. The result is rounded towards + * zero. + */ + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /** + * @dev Returns the ceiling of the division of two numbers. + * + * This differs from standard division with `/` in that it rounds towards infinity instead + * of rounding towards zero. + */ + function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + Panic.panic(Panic.DIVISION_BY_ZERO); + } + + // The following calculation ensures accurate ceiling division without overflow. + // Since a is non-zero, (a - 1) / b will not overflow. + // The largest possible result occurs when (a - 1) / b is type(uint256).max, + // but the largest value we can obtain is type(uint256).max - 1, which happens + // when a = type(uint256).max and b = 1. + unchecked { + return SafeCast.toUint(a > 0) * ((a - 1) / b + 1); + } + } + + /** + * @dev Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or + * denominator == 0. + * + * Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv) with further edits by + * Uniswap Labs also under MIT license. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) { + unchecked { + // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use + // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2²⁵⁶ + prod0. + uint256 prod0 = x * y; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(x, y, not(0)) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division. + if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. + return prod0 / denominator; + } + + // Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0. + if (denominator <= prod1) { + Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW)); + } + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0]. + uint256 remainder; + assembly { + // Compute remainder using mulmod. + remainder := mulmod(x, y, denominator) + + // Subtract 256 bit number from 512 bit number. + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator and compute largest power of two divisor of denominator. + // Always >= 1. See https://cs.stackexchange.com/q/138556/92363. + + uint256 twos = denominator & (0 - denominator); + assembly { + // Divide denominator by twos. + denominator := div(denominator, twos) + + // Divide [prod1 prod0] by twos. + prod0 := div(prod0, twos) + + // Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one. + twos := add(div(sub(0, twos), twos), 1) + } + + // Shift in bits from prod1 into prod0. + prod0 |= prod1 * twos; + + // Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such + // that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for + // four bits. That is, denominator * inv ≡ 1 mod 2⁴. + uint256 inverse = (3 * denominator) ^ 2; + + // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also + // works in modular arithmetic, doubling the correct bits in each step. + inverse *= 2 - denominator * inverse; // inverse mod 2⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2¹⁶ + inverse *= 2 - denominator * inverse; // inverse mod 2³² + inverse *= 2 - denominator * inverse; // inverse mod 2⁶⁴ + inverse *= 2 - denominator * inverse; // inverse mod 2¹²⁸ + inverse *= 2 - denominator * inverse; // inverse mod 2²⁵⁶ + + // Because the division is now exact we can divide by multiplying with the modular inverse of denominator. + // This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is + // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inverse; + return result; + } + } + + /** + * @dev Calculates x * y / denominator with full precision, following the selected rounding direction. + */ + function mulDiv(uint256 x, uint256 y, uint256 denominator, Rounding rounding) internal pure returns (uint256) { + return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0); + } + + /** + * @dev Calculate the modular multiplicative inverse of a number in Z/nZ. + * + * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0. + * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible. + * + * If the input value is not inversible, 0 is returned. + * + * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the + * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}. + */ + function invMod(uint256 a, uint256 n) internal pure returns (uint256) { + unchecked { + if (n == 0) return 0; + + // The inverse modulo is calculated using the Extended Euclidean Algorithm (iterative version) + // Used to compute integers x and y such that: ax + ny = gcd(a, n). + // When the gcd is 1, then the inverse of a modulo n exists and it's x. + // ax + ny = 1 + // ax = 1 + (-y)n + // ax ≡ 1 (mod n) # x is the inverse of a modulo n + + // If the remainder is 0 the gcd is n right away. + uint256 remainder = a % n; + uint256 gcd = n; + + // Therefore the initial coefficients are: + // ax + ny = gcd(a, n) = n + // 0a + 1n = n + int256 x = 0; + int256 y = 1; + + while (remainder != 0) { + uint256 quotient = gcd / remainder; + + (gcd, remainder) = ( + // The old remainder is the next gcd to try. + remainder, + // Compute the next remainder. + // Can't overflow given that (a % gcd) * (gcd // (a % gcd)) <= gcd + // where gcd is at most n (capped to type(uint256).max) + gcd - remainder * quotient + ); + + (x, y) = ( + // Increment the coefficient of a. + y, + // Decrement the coefficient of n. + // Can overflow, but the result is casted to uint256 so that the + // next value of y is "wrapped around" to a value between 0 and n - 1. + x - y * int256(quotient) + ); + } + + if (gcd != 1) return 0; // No inverse exists. + return ternary(x < 0, n - uint256(-x), uint256(x)); // Wrap the result if it's negative. + } + } + + /** + * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`. + * + * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is + * prime, then `a**(p-1) ≡ 1 mod p`. As a consequence, we have `a * a**(p-2) ≡ 1 mod p`, which means that + * `a**(p-2)` is the modular multiplicative inverse of a in Fp. + * + * NOTE: this function does NOT check that `p` is a prime greater than `2`. + */ + function invModPrime(uint256 a, uint256 p) internal view returns (uint256) { + unchecked { + return Math.modExp(a, p - 2, p); + } + } + + /** + * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m) + * + * Requirements: + * - modulus can't be zero + * - underlying staticcall to precompile must succeed + * + * IMPORTANT: The result is only valid if the underlying call succeeds. When using this function, make + * sure the chain you're using it on supports the precompiled contract for modular exponentiation + * at address 0x05 as specified in https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, + * the underlying function will succeed given the lack of a revert, but the result may be incorrectly + * interpreted as 0. + */ + function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) { + (bool success, uint256 result) = tryModExp(b, e, m); + if (!success) { + Panic.panic(Panic.DIVISION_BY_ZERO); + } + return result; + } + + /** + * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m). + * It includes a success flag indicating if the operation succeeded. Operation will be marked as failed if trying + * to operate modulo 0 or if the underlying precompile reverted. + * + * IMPORTANT: The result is only valid if the success flag is true. When using this function, make sure the chain + * you're using it on supports the precompiled contract for modular exponentiation at address 0x05 as specified in + * https://eips.ethereum.org/EIPS/eip-198[EIP-198]. Otherwise, the underlying function will succeed given the lack + * of a revert, but the result may be incorrectly interpreted as 0. + */ + function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) { + if (m == 0) return (false, 0); + assembly ("memory-safe") { + let ptr := mload(0x40) + // | Offset | Content | Content (Hex) | + // |-----------|------------|--------------------------------------------------------------------| + // | 0x00:0x1f | size of b | 0x0000000000000000000000000000000000000000000000000000000000000020 | + // | 0x20:0x3f | size of e | 0x0000000000000000000000000000000000000000000000000000000000000020 | + // | 0x40:0x5f | size of m | 0x0000000000000000000000000000000000000000000000000000000000000020 | + // | 0x60:0x7f | value of b | 0x<.............................................................b> | + // | 0x80:0x9f | value of e | 0x<.............................................................e> | + // | 0xa0:0xbf | value of m | 0x<.............................................................m> | + mstore(ptr, 0x20) + mstore(add(ptr, 0x20), 0x20) + mstore(add(ptr, 0x40), 0x20) + mstore(add(ptr, 0x60), b) + mstore(add(ptr, 0x80), e) + mstore(add(ptr, 0xa0), m) + + // Given the result < m, it's guaranteed to fit in 32 bytes, + // so we can use the memory scratch space located at offset 0. + success := staticcall(gas(), 0x05, ptr, 0xc0, 0x00, 0x20) + result := mload(0x00) + } + } + + /** + * @dev Variant of {modExp} that supports inputs of arbitrary length. + */ + function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) { + (bool success, bytes memory result) = tryModExp(b, e, m); + if (!success) { + Panic.panic(Panic.DIVISION_BY_ZERO); + } + return result; + } + + /** + * @dev Variant of {tryModExp} that supports inputs of arbitrary length. + */ + function tryModExp( + bytes memory b, + bytes memory e, + bytes memory m + ) internal view returns (bool success, bytes memory result) { + if (_zeroBytes(m)) return (false, new bytes(0)); + + uint256 mLen = m.length; + + // Encode call args in result and move the free memory pointer + result = abi.encodePacked(b.length, e.length, mLen, b, e, m); + + assembly ("memory-safe") { + let dataPtr := add(result, 0x20) + // Write result on top of args to avoid allocating extra memory. + success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen) + // Overwrite the length. + // result.length > returndatasize() is guaranteed because returndatasize() == m.length + mstore(result, mLen) + // Set the memory pointer after the returned data. + mstore(0x40, add(dataPtr, mLen)) + } + } + + /** + * @dev Returns whether the provided byte array is zero. + */ + function _zeroBytes(bytes memory byteArray) private pure returns (bool) { + for (uint256 i = 0; i < byteArray.length; ++i) { + if (byteArray[i] != 0) { + return false; + } + } + return true; + } + + /** + * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded + * towards zero. + * + * This method is based on Newton's method for computing square roots; the algorithm is restricted to only + * using integer operations. + */ + function sqrt(uint256 a) internal pure returns (uint256) { + unchecked { + // Take care of easy edge cases when a == 0 or a == 1 + if (a <= 1) { + return a; + } + + // In this function, we use Newton's method to get a root of `f(x) := x² - a`. It involves building a + // sequence x_n that converges toward sqrt(a). For each iteration x_n, we also define the error between + // the current value as `ε_n = | x_n - sqrt(a) |`. + // + // For our first estimation, we consider `e` the smallest power of 2 which is bigger than the square root + // of the target. (i.e. `2**(e-1) ≤ sqrt(a) < 2**e`). We know that `e ≤ 128` because `(2¹²⁸)² = 2²⁵⁶` is + // bigger than any uint256. + // + // By noticing that + // `2**(e-1) ≤ sqrt(a) < 2**e → (2**(e-1))² ≤ a < (2**e)² → 2**(2*e-2) ≤ a < 2**(2*e)` + // we can deduce that `e - 1` is `log2(a) / 2`. We can thus compute `x_n = 2**(e-1)` using a method similar + // to the msb function. + uint256 aa = a; + uint256 xn = 1; + + if (aa >= (1 << 128)) { + aa >>= 128; + xn <<= 64; + } + if (aa >= (1 << 64)) { + aa >>= 64; + xn <<= 32; + } + if (aa >= (1 << 32)) { + aa >>= 32; + xn <<= 16; + } + if (aa >= (1 << 16)) { + aa >>= 16; + xn <<= 8; + } + if (aa >= (1 << 8)) { + aa >>= 8; + xn <<= 4; + } + if (aa >= (1 << 4)) { + aa >>= 4; + xn <<= 2; + } + if (aa >= (1 << 2)) { + xn <<= 1; + } + + // We now have x_n such that `x_n = 2**(e-1) ≤ sqrt(a) < 2**e = 2 * x_n`. This implies ε_n ≤ 2**(e-1). + // + // We can refine our estimation by noticing that the middle of that interval minimizes the error. + // If we move x_n to equal 2**(e-1) + 2**(e-2), then we reduce the error to ε_n ≤ 2**(e-2). + // This is going to be our x_0 (and ε_0) + xn = (3 * xn) >> 1; // ε_0 := | x_0 - sqrt(a) | ≤ 2**(e-2) + + // From here, Newton's method give us: + // x_{n+1} = (x_n + a / x_n) / 2 + // + // One should note that: + // x_{n+1}² - a = ((x_n + a / x_n) / 2)² - a + // = ((x_n² + a) / (2 * x_n))² - a + // = (x_n⁴ + 2 * a * x_n² + a²) / (4 * x_n²) - a + // = (x_n⁴ + 2 * a * x_n² + a² - 4 * a * x_n²) / (4 * x_n²) + // = (x_n⁴ - 2 * a * x_n² + a²) / (4 * x_n²) + // = (x_n² - a)² / (2 * x_n)² + // = ((x_n² - a) / (2 * x_n))² + // ≥ 0 + // Which proves that for all n ≥ 1, sqrt(a) ≤ x_n + // + // This gives us the proof of quadratic convergence of the sequence: + // ε_{n+1} = | x_{n+1} - sqrt(a) | + // = | (x_n + a / x_n) / 2 - sqrt(a) | + // = | (x_n² + a - 2*x_n*sqrt(a)) / (2 * x_n) | + // = | (x_n - sqrt(a))² / (2 * x_n) | + // = | ε_n² / (2 * x_n) | + // = ε_n² / | (2 * x_n) | + // + // For the first iteration, we have a special case where x_0 is known: + // ε_1 = ε_0² / | (2 * x_0) | + // ≤ (2**(e-2))² / (2 * (2**(e-1) + 2**(e-2))) + // ≤ 2**(2*e-4) / (3 * 2**(e-1)) + // ≤ 2**(e-3) / 3 + // ≤ 2**(e-3-log2(3)) + // ≤ 2**(e-4.5) + // + // For the following iterations, we use the fact that, 2**(e-1) ≤ sqrt(a) ≤ x_n: + // ε_{n+1} = ε_n² / | (2 * x_n) | + // ≤ (2**(e-k))² / (2 * 2**(e-1)) + // ≤ 2**(2*e-2*k) / 2**e + // ≤ 2**(e-2*k) + xn = (xn + a / xn) >> 1; // ε_1 := | x_1 - sqrt(a) | ≤ 2**(e-4.5) -- special case, see above + xn = (xn + a / xn) >> 1; // ε_2 := | x_2 - sqrt(a) | ≤ 2**(e-9) -- general case with k = 4.5 + xn = (xn + a / xn) >> 1; // ε_3 := | x_3 - sqrt(a) | ≤ 2**(e-18) -- general case with k = 9 + xn = (xn + a / xn) >> 1; // ε_4 := | x_4 - sqrt(a) | ≤ 2**(e-36) -- general case with k = 18 + xn = (xn + a / xn) >> 1; // ε_5 := | x_5 - sqrt(a) | ≤ 2**(e-72) -- general case with k = 36 + xn = (xn + a / xn) >> 1; // ε_6 := | x_6 - sqrt(a) | ≤ 2**(e-144) -- general case with k = 72 + + // Because e ≤ 128 (as discussed during the first estimation phase), we know have reached a precision + // ε_6 ≤ 2**(e-144) < 1. Given we're operating on integers, then we can ensure that xn is now either + // sqrt(a) or sqrt(a) + 1. + return xn - SafeCast.toUint(xn > a / xn); + } + } + + /** + * @dev Calculates sqrt(a), following the selected rounding direction. + */ + function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = sqrt(a); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && result * result < a); + } + } + + /** + * @dev Return the log in base 2 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log2(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + uint256 exp; + unchecked { + exp = 128 * SafeCast.toUint(value > (1 << 128) - 1); + value >>= exp; + result += exp; + + exp = 64 * SafeCast.toUint(value > (1 << 64) - 1); + value >>= exp; + result += exp; + + exp = 32 * SafeCast.toUint(value > (1 << 32) - 1); + value >>= exp; + result += exp; + + exp = 16 * SafeCast.toUint(value > (1 << 16) - 1); + value >>= exp; + result += exp; + + exp = 8 * SafeCast.toUint(value > (1 << 8) - 1); + value >>= exp; + result += exp; + + exp = 4 * SafeCast.toUint(value > (1 << 4) - 1); + value >>= exp; + result += exp; + + exp = 2 * SafeCast.toUint(value > (1 << 2) - 1); + value >>= exp; + result += exp; + + result += SafeCast.toUint(value > 1); + } + return result; + } + + /** + * @dev Return the log in base 2, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log2(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log2(value); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << result < value); + } + } + + /** + * @dev Return the log in base 10 of a positive value rounded towards zero. + * Returns 0 if given 0. + */ + function log10(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + unchecked { + if (value >= 10 ** 64) { + value /= 10 ** 64; + result += 64; + } + if (value >= 10 ** 32) { + value /= 10 ** 32; + result += 32; + } + if (value >= 10 ** 16) { + value /= 10 ** 16; + result += 16; + } + if (value >= 10 ** 8) { + value /= 10 ** 8; + result += 8; + } + if (value >= 10 ** 4) { + value /= 10 ** 4; + result += 4; + } + if (value >= 10 ** 2) { + value /= 10 ** 2; + result += 2; + } + if (value >= 10 ** 1) { + result += 1; + } + } + return result; + } + + /** + * @dev Return the log in base 10, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log10(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log10(value); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 10 ** result < value); + } + } + + /** + * @dev Return the log in base 256 of a positive value rounded towards zero. + * Returns 0 if given 0. + * + * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. + */ + function log256(uint256 value) internal pure returns (uint256) { + uint256 result = 0; + uint256 isGt; + unchecked { + isGt = SafeCast.toUint(value > (1 << 128) - 1); + value >>= isGt * 128; + result += isGt * 16; + + isGt = SafeCast.toUint(value > (1 << 64) - 1); + value >>= isGt * 64; + result += isGt * 8; + + isGt = SafeCast.toUint(value > (1 << 32) - 1); + value >>= isGt * 32; + result += isGt * 4; + + isGt = SafeCast.toUint(value > (1 << 16) - 1); + value >>= isGt * 16; + result += isGt * 2; + + result += SafeCast.toUint(value > (1 << 8) - 1); + } + return result; + } + + /** + * @dev Return the log in base 256, following the selected rounding direction, of a positive value. + * Returns 0 if given 0. + */ + function log256(uint256 value, Rounding rounding) internal pure returns (uint256) { + unchecked { + uint256 result = log256(value); + return result + SafeCast.toUint(unsignedRoundsUp(rounding) && 1 << (result << 3) < value); + } + } + + /** + * @dev Returns whether a provided rounding mode is considered rounding up for unsigned integers. + */ + function unsignedRoundsUp(Rounding rounding) internal pure returns (bool) { + return uint8(rounding) % 2 == 1; + } +} + + +// File @openzeppelin/contracts/utils/math/SignedMath.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SignedMath.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Standard signed math utilities missing in the Solidity language. + */ +library SignedMath { + /** + * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant. + * + * IMPORTANT: This function may reduce bytecode size and consume less gas when used standalone. + * However, the compiler may optimize Solidity ternary operations (i.e. `a ? b : c`) to only compute + * one branch when needed, making this function more expensive. + */ + function ternary(bool condition, int256 a, int256 b) internal pure returns (int256) { + unchecked { + // branchless ternary works because: + // b ^ (a ^ b) == a + // b ^ 0 == b + return b ^ ((a ^ b) * int256(SafeCast.toUint(condition))); + } + } + + /** + * @dev Returns the largest of two signed numbers. + */ + function max(int256 a, int256 b) internal pure returns (int256) { + return ternary(a > b, a, b); + } + + /** + * @dev Returns the smallest of two signed numbers. + */ + function min(int256 a, int256 b) internal pure returns (int256) { + return ternary(a < b, a, b); + } + + /** + * @dev Returns the average of two signed numbers without overflow. + * The result is rounded towards zero. + */ + function average(int256 a, int256 b) internal pure returns (int256) { + // Formula from the book "Hacker's Delight" + int256 x = (a & b) + ((a ^ b) >> 1); + return x + (int256(uint256(x) >> 255) & (a ^ b)); + } + + /** + * @dev Returns the absolute unsigned value of a signed value. + */ + function abs(int256 n) internal pure returns (uint256) { + unchecked { + // Formula from the "Bit Twiddling Hacks" by Sean Eron Anderson. + // Since `n` is a signed integer, the generated bytecode will use the SAR opcode to perform the right shift, + // taking advantage of the most significant (or "sign" bit) in two's complement representation. + // This opcode adds new most significant bits set to the value of the previous most significant bit. As a result, + // the mask will either be `bytes32(0)` (if n is positive) or `~bytes32(0)` (if n is negative). + int256 mask = n >> 255; + + // A `bytes32(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it. + return uint256((n + mask) ^ mask); + } + } +} + + +// File @openzeppelin/contracts/utils/Strings.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (utils/Strings.sol) + +pragma solidity ^0.8.20; + + + +/** + * @dev String operations. + */ +library Strings { + using SafeCast for *; + + bytes16 private constant HEX_DIGITS = "0123456789abcdef"; + uint8 private constant ADDRESS_LENGTH = 20; + + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + + /** + * @dev The string being parsed contains characters that are not in scope of the given base. + */ + error StringsInvalidChar(); + + /** + * @dev The string being parsed is not a properly formatted address. + */ + error StringsInvalidAddressFormat(); + + /** + * @dev Converts a `uint256` to its ASCII `string` decimal representation. + */ + function toString(uint256 value) internal pure returns (string memory) { + unchecked { + uint256 length = Math.log10(value) + 1; + string memory buffer = new string(length); + uint256 ptr; + assembly ("memory-safe") { + ptr := add(buffer, add(32, length)) + } + while (true) { + ptr--; + assembly ("memory-safe") { + mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) + } + value /= 10; + if (value == 0) break; + } + return buffer; + } + } + + /** + * @dev Converts a `int256` to its ASCII `string` decimal representation. + */ + function toStringSigned(int256 value) internal pure returns (string memory) { + return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + unchecked { + return toHexString(value, Math.log256(value) + 1); + } + } + + /** + * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + uint256 localValue = value; + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = HEX_DIGITS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); + } + return string(buffer); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal + * representation. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH); + } + + /** + * @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal + * representation, according to EIP-55. + */ + function toChecksumHexString(address addr) internal pure returns (string memory) { + bytes memory buffer = bytes(toHexString(addr)); + + // hash the hex part of buffer (skip length + 2 bytes, length 40) + uint256 hashValue; + assembly ("memory-safe") { + hashValue := shr(96, keccak256(add(buffer, 0x22), 40)) + } + + for (uint256 i = 41; i > 1; --i) { + // possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f) + if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) { + // case shift by xoring with 0x20 + buffer[i] ^= 0x20; + } + hashValue >>= 4; + } + return string(buffer); + } + + /** + * @dev Returns true if the two strings are equal. + */ + function equal(string memory a, string memory b) internal pure returns (bool) { + return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + } + + /** + * @dev Parse a decimal string and returns the value as a `uint256`. + * + * Requirements: + * - The string must be formatted as `[0-9]*` + * - The result must fit into an `uint256` type + */ + function parseUint(string memory input) internal pure returns (uint256) { + return parseUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseUint} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `[0-9]*` + * - The result must fit into an `uint256` type + */ + function parseUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { + (bool success, uint256 value) = tryParseUint(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseUint-string} that returns false if the parsing fails because of an invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) { + return _tryParseUintUncheckedBounds(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseUint-string-uint256-uint256} that returns false if the parsing fails because of an invalid + * character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseUint( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, uint256 value) { + if (end > bytes(input).length || begin > end) return (false, 0); + return _tryParseUintUncheckedBounds(input, begin, end); + } + + /** + * @dev Implementation of {tryParseUint} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. + */ + function _tryParseUintUncheckedBounds( + string memory input, + uint256 begin, + uint256 end + ) private pure returns (bool success, uint256 value) { + bytes memory buffer = bytes(input); + + uint256 result = 0; + for (uint256 i = begin; i < end; ++i) { + uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i))); + if (chr > 9) return (false, 0); + result *= 10; + result += chr; + } + return (true, result); + } + + /** + * @dev Parse a decimal string and returns the value as a `int256`. + * + * Requirements: + * - The string must be formatted as `[-+]?[0-9]*` + * - The result must fit in an `int256` type. + */ + function parseInt(string memory input) internal pure returns (int256) { + return parseInt(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseInt-string} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `[-+]?[0-9]*` + * - The result must fit in an `int256` type. + */ + function parseInt(string memory input, uint256 begin, uint256 end) internal pure returns (int256) { + (bool success, int256 value) = tryParseInt(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseInt-string} that returns false if the parsing fails because of an invalid character or if + * the result does not fit in a `int256`. + * + * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`. + */ + function tryParseInt(string memory input) internal pure returns (bool success, int256 value) { + return _tryParseIntUncheckedBounds(input, 0, bytes(input).length); + } + + uint256 private constant ABS_MIN_INT256 = 2 ** 255; + + /** + * @dev Variant of {parseInt-string-uint256-uint256} that returns false if the parsing fails because of an invalid + * character or if the result does not fit in a `int256`. + * + * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`. + */ + function tryParseInt( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, int256 value) { + if (end > bytes(input).length || begin > end) return (false, 0); + return _tryParseIntUncheckedBounds(input, begin, end); + } + + /** + * @dev Implementation of {tryParseInt} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. + */ + function _tryParseIntUncheckedBounds( + string memory input, + uint256 begin, + uint256 end + ) private pure returns (bool success, int256 value) { + bytes memory buffer = bytes(input); + + // Check presence of a negative sign. + bytes1 sign = begin == end ? bytes1(0) : bytes1(_unsafeReadBytesOffset(buffer, begin)); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + bool positiveSign = sign == bytes1("+"); + bool negativeSign = sign == bytes1("-"); + uint256 offset = (positiveSign || negativeSign).toUint(); + + (bool absSuccess, uint256 absValue) = tryParseUint(input, begin + offset, end); + + if (absSuccess && absValue < ABS_MIN_INT256) { + return (true, negativeSign ? -int256(absValue) : int256(absValue)); + } else if (absSuccess && negativeSign && absValue == ABS_MIN_INT256) { + return (true, type(int256).min); + } else return (false, 0); + } + + /** + * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as a `uint256`. + * + * Requirements: + * - The string must be formatted as `(0x)?[0-9a-fA-F]*` + * - The result must fit in an `uint256` type. + */ + function parseHexUint(string memory input) internal pure returns (uint256) { + return parseHexUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseHexUint} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `(0x)?[0-9a-fA-F]*` + * - The result must fit in an `uint256` type. + */ + function parseHexUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { + (bool success, uint256 value) = tryParseHexUint(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseHexUint-string} that returns false if the parsing fails because of an invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseHexUint(string memory input) internal pure returns (bool success, uint256 value) { + return _tryParseHexUintUncheckedBounds(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseHexUint-string-uint256-uint256} that returns false if the parsing fails because of an + * invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseHexUint( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, uint256 value) { + if (end > bytes(input).length || begin > end) return (false, 0); + return _tryParseHexUintUncheckedBounds(input, begin, end); + } + + /** + * @dev Implementation of {tryParseHexUint} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. + */ + function _tryParseHexUintUncheckedBounds( + string memory input, + uint256 begin, + uint256 end + ) private pure returns (bool success, uint256 value) { + bytes memory buffer = bytes(input); + + // skip 0x prefix if present + bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + uint256 offset = hasPrefix.toUint() * 2; + + uint256 result = 0; + for (uint256 i = begin + offset; i < end; ++i) { + uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i))); + if (chr > 15) return (false, 0); + result *= 16; + unchecked { + // Multiplying by 16 is equivalent to a shift of 4 bits (with additional overflow check). + // This guaratees that adding a value < 16 will not cause an overflow, hence the unchecked. + result += chr; + } + } + return (true, result); + } + + /** + * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as an `address`. + * + * Requirements: + * - The string must be formatted as `(0x)?[0-9a-fA-F]{40}` + */ + function parseAddress(string memory input) internal pure returns (address) { + return parseAddress(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseAddress} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `(0x)?[0-9a-fA-F]{40}` + */ + function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) { + (bool success, address value) = tryParseAddress(input, begin, end); + if (!success) revert StringsInvalidAddressFormat(); + return value; + } + + /** + * @dev Variant of {parseAddress-string} that returns false if the parsing fails because the input is not a properly + * formatted address. See {parseAddress} requirements. + */ + function tryParseAddress(string memory input) internal pure returns (bool success, address value) { + return tryParseAddress(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseAddress-string-uint256-uint256} that returns false if the parsing fails because input is not a properly + * formatted address. See {parseAddress} requirements. + */ + function tryParseAddress( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, address value) { + if (end > bytes(input).length || begin > end) return (false, address(0)); + + bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + uint256 expectedLength = 40 + hasPrefix.toUint() * 2; + + // check that input is the correct length + if (end - begin == expectedLength) { + // length guarantees that this does not overflow, and value is at most type(uint160).max + (bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end); + return (s, address(uint160(v))); + } else { + return (false, address(0)); + } + } + + function _tryParseChr(bytes1 chr) private pure returns (uint8) { + uint8 value = uint8(chr); + + // Try to parse `chr`: + // - Case 1: [0-9] + // - Case 2: [a-f] + // - Case 3: [A-F] + // - otherwise not supported + unchecked { + if (value > 47 && value < 58) value -= 48; + else if (value > 96 && value < 103) value -= 87; + else if (value > 64 && value < 71) value -= 55; + else return type(uint8).max; + } + + return value; + } + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } +} + + +// File @openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/MessageHashUtils.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Signature message hash utilities for producing digests to be consumed by {ECDSA} recovery or signing. + * + * The library provides methods for generating a hash of a message that conforms to the + * https://eips.ethereum.org/EIPS/eip-191[ERC-191] and https://eips.ethereum.org/EIPS/eip-712[EIP 712] + * specifications. + */ +library MessageHashUtils { + /** + * @dev Returns the keccak256 digest of an ERC-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing a bytes32 `messageHash` with + * `"\x19Ethereum Signed Message:\n32"` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * NOTE: The `messageHash` parameter is intended to be the result of hashing a raw message with + * keccak256, although any bytes32 value can be safely used because the final digest will + * be re-hashed. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) { + assembly ("memory-safe") { + mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash + mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix + digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20) + } + } + + /** + * @dev Returns the keccak256 digest of an ERC-191 signed data with version + * `0x45` (`personal_sign` messages). + * + * The digest is calculated by prefixing an arbitrary `message` with + * `"\x19Ethereum Signed Message:\n" + len(message)` and hashing the result. It corresponds with the + * hash signed when using the https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] JSON-RPC method. + * + * See {ECDSA-recover}. + */ + function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) { + return + keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message)); + } + + /** + * @dev Returns the keccak256 digest of an ERC-191 signed data with version + * `0x00` (data with intended validator). + * + * The digest is calculated by prefixing an arbitrary `data` with `"\x19\x00"` and the intended + * `validator` address. Then hashing the result. + * + * See {ECDSA-recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"19_00", validator, data)); + } + + /** + * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`). + * + * The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them with + * `\x19\x01` and hashing the result. It corresponds to the hash signed by the + * https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of EIP-712. + * + * See {ECDSA-recover}. + */ + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(ptr, hex"19_01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + digest := keccak256(ptr, 0x42) + } + } +} + + +// File @openzeppelin/contracts/utils/StorageSlot.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. + +pragma solidity ^0.8.20; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. + * + * Example usage to set ERC-1967 implementation slot: + * ```solidity + * contract ERC1967 { + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * ``` + * + * TIP: Consider using this library along with {SlotDerivation}. + */ +library StorageSlot { + struct AddressSlot { + address value; + } + + struct BooleanSlot { + bool value; + } + + struct Bytes32Slot { + bytes32 value; + } + + struct Uint256Slot { + uint256 value; + } + + struct Int256Slot { + int256 value; + } + + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + + /** + * @dev Returns an `AddressSlot` with member `value` located at `slot`. + */ + function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `BooleanSlot` with member `value` located at `slot`. + */ + function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `Bytes32Slot` with member `value` located at `slot`. + */ + function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `Uint256Slot` with member `value` located at `slot`. + */ + function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `Int256Slot` with member `value` located at `slot`. + */ + function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns a `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + assembly ("memory-safe") { + r.slot := store.slot + } + } + + /** + * @dev Returns a `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + assembly ("memory-safe") { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + assembly ("memory-safe") { + r.slot := store.slot + } + } +} + + +// File @openzeppelin/contracts/utils/ShortStrings.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ShortStrings.sol) + +pragma solidity ^0.8.20; + +// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | +// | length | 0x BB | +type ShortString is bytes32; + +/** + * @dev This library provides functions to convert short memory strings + * into a `ShortString` type that can be used as an immutable variable. + * + * Strings of arbitrary length can be optimized using this library if + * they are short enough (up to 31 bytes) by packing them with their + * length (1 byte) in a single EVM word (32 bytes). Additionally, a + * fallback mechanism can be used for every other case. + * + * Usage example: + * + * ```solidity + * contract Named { + * using ShortStrings for *; + * + * ShortString private immutable _name; + * string private _nameFallback; + * + * constructor(string memory contractName) { + * _name = contractName.toShortStringWithFallback(_nameFallback); + * } + * + * function name() external view returns (string memory) { + * return _name.toStringWithFallback(_nameFallback); + * } + * } + * ``` + */ +library ShortStrings { + // Used as an identifier for strings longer than 31 bytes. + bytes32 private constant FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; + + error StringTooLong(string str); + error InvalidShortString(); + + /** + * @dev Encode a string of at most 31 chars into a `ShortString`. + * + * This will trigger a `StringTooLong` error is the input string is too long. + */ + function toShortString(string memory str) internal pure returns (ShortString) { + bytes memory bstr = bytes(str); + if (bstr.length > 31) { + revert StringTooLong(str); + } + return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); + } + + /** + * @dev Decode a `ShortString` back to a "normal" string. + */ + function toString(ShortString sstr) internal pure returns (string memory) { + uint256 len = byteLength(sstr); + // using `new string(len)` would work locally but is not memory safe. + string memory str = new string(32); + assembly ("memory-safe") { + mstore(str, len) + mstore(add(str, 0x20), sstr) + } + return str; + } + + /** + * @dev Return the length of a `ShortString`. + */ + function byteLength(ShortString sstr) internal pure returns (uint256) { + uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; + if (result > 31) { + revert InvalidShortString(); + } + return result; + } + + /** + * @dev Encode a string into a `ShortString`, or write it to storage if it is too long. + */ + function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) { + if (bytes(value).length < 32) { + return toShortString(value); + } else { + StorageSlot.getStringSlot(store).value = value; + return ShortString.wrap(FALLBACK_SENTINEL); + } + } + + /** + * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + */ + function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { + if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { + return toString(value); + } else { + return store; + } + } + + /** + * @dev Return the length of a string that was encoded to `ShortString` or written to storage using + * {setWithFallback}. + * + * WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of + * actual characters as the UTF-8 encoding of a single character can span over multiple bytes. + */ + function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) { + if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { + return byteLength(value); + } else { + return bytes(store).length; + } + } +} + + +// File @openzeppelin/contracts/utils/cryptography/EIP712.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/EIP712.sol) + +pragma solidity ^0.8.20; + + + +/** + * @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data. + * + * The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose + * encoding is very generic and therefore its implementation in Solidity is not feasible, thus this contract + * does not implement the encoding itself. Protocols need to implement the type-specific encoding they need in order to + * produce the hash of their typed data using a combination of `abi.encode` and `keccak256`. + * + * This contract implements the EIP-712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding + * scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA + * ({_hashTypedDataV4}). + * + * The implementation of the domain separator was designed to be as efficient as possible while still properly updating + * the chain id to protect against replay attacks on an eventual fork of the chain. + * + * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method + * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. + * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + * + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ +abstract contract EIP712 is IERC5267 { + using ShortStrings for *; + + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to + // invalidate the cached domain separator if the chain id changes. + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; + + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; + + ShortString private immutable _name; + ShortString private immutable _version; + string private _nameFallback; + string private _versionFallback; + + /** + * @dev Initializes the domain separator and parameter caches. + * + * The meaning of `name` and `version` is specified in + * https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP-712]: + * + * - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol. + * - `version`: the current major version of the signing domain. + * + * NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart + * contract upgrade]. + */ + constructor(string memory name, string memory version) { + _name = name.toShortStringWithFallback(_nameFallback); + _version = version.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(name)); + _hashedVersion = keccak256(bytes(version)); + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; + } else { + return _buildDomainSeparator(); + } + } + + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + /** + * @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this + * function returns the hash of the fully encoded EIP712 message for this domain. + * + * This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example: + * + * ```solidity + * bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + * keccak256("Mail(address to,string contents)"), + * mailTo, + * keccak256(bytes(mailContents)) + * ))); + * address signer = ECDSA.recover(digest, signature); + * ``` + */ + function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { + return MessageHashUtils.toTypedDataHash(_domainSeparatorV4(), structHash); + } + + /** + * @dev See {IERC-5267}. + */ + function eip712Domain() + public + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _EIP712Name(), + _EIP712Version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: By default this function reads _name which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Name() internal view returns (string memory) { + return _name.toStringWithFallback(_nameFallback); + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: By default this function reads _version which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Version() internal view returns (string memory) { + return _version.toStringWithFallback(_versionFallback); + } +} + + +// File @openzeppelin/contracts/utils/Nonces.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Nonces.sol) +pragma solidity ^0.8.20; + +/** + * @dev Provides tracking nonces for addresses. Nonces will only increment. + */ +abstract contract Nonces { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); + + mapping(address account => uint256) private _nonces; + + /** + * @dev Returns the next unused nonce for an address. + */ + function nonces(address owner) public view virtual returns (uint256) { + return _nonces[owner]; + } + + /** + * @dev Consumes a nonce. + * + * Returns the current value and increments nonce. + */ + function _useNonce(address owner) internal virtual returns (uint256) { + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return _nonces[owner]++; + } + } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + */ + function _useCheckedNonce(address owner, uint256 nonce) internal virtual { + uint256 current = _useNonce(owner); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + } +} + + +// File @openzeppelin/contracts/utils/structs/Checkpoints.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/Checkpoints.sol) +// This file was procedurally generated from scripts/generate/templates/Checkpoints.js. + +pragma solidity ^0.8.20; + +/** + * @dev This library defines the `Trace*` struct, for checkpointing values as they change at different points in + * time, and later looking up past values by block number. See {Votes} as an example. + * + * To create a history of checkpoints define a variable type `Checkpoints.Trace*` in your contract, and store a new + * checkpoint for the current transaction block using the {push} function. + */ +library Checkpoints { + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); + + struct Trace224 { + Checkpoint224[] _checkpoints; + } + + struct Checkpoint224 { + uint32 _key; + uint224 _value; + } + + /** + * @dev Pushes a (`key`, `value`) pair into a Trace224 so that it is stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint32).max` key set will disable the + * library. + */ + function push( + Trace224 storage self, + uint32 key, + uint224 value + ) internal returns (uint224 oldValue, uint224 newValue) { + return _insert(self._checkpoints, key, value); + } + + /** + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if + * there is none. + */ + function lowerLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); + return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + */ + function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high + * keys). + */ + function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. + */ + function latest(Trace224 storage self) internal view returns (uint224) { + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value + * in the most recent checkpoint. + */ + function latestCheckpoint(Trace224 storage self) internal view returns (bool exists, uint32 _key, uint224 _value) { + uint256 pos = self._checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint224 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); + return (true, ckpt._key, ckpt._value); + } + } + + /** + * @dev Returns the number of checkpoint. + */ + function length(Trace224 storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace224 storage self, uint32 pos) internal view returns (Checkpoint224 memory) { + return self._checkpoints[pos]; + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, + * or by updating the last one. + */ + function _insert( + Checkpoint224[] storage self, + uint32 key, + uint224 value + ) private returns (uint224 oldValue, uint224 newValue) { + uint256 pos = self.length; + + if (pos > 0) { + Checkpoint224 storage last = _unsafeAccess(self, pos - 1); + uint32 lastKey = last._key; + uint224 lastValue = last._value; + + // Checkpoint keys must be non-decreasing. + if (lastKey > key) { + revert CheckpointUnorderedInsertion(); + } + + // Update or push new checkpoint + if (lastKey == key) { + last._value = value; + } else { + self.push(Checkpoint224({_key: key, _value: value})); + } + return (lastValue, value); + } else { + self.push(Checkpoint224({_key: key, _value: value})); + return (0, value); + } + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _upperBinaryLookup( + Checkpoint224[] storage self, + uint32 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key > key) { + high = mid; + } else { + low = mid + 1; + } + } + return high; + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _lowerBinaryLookup( + Checkpoint224[] storage self, + uint32 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess( + Checkpoint224[] storage self, + uint256 pos + ) private pure returns (Checkpoint224 storage result) { + assembly { + mstore(0, self.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } + + struct Trace208 { + Checkpoint208[] _checkpoints; + } + + struct Checkpoint208 { + uint48 _key; + uint208 _value; + } + + /** + * @dev Pushes a (`key`, `value`) pair into a Trace208 so that it is stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint48).max` key set will disable the + * library. + */ + function push( + Trace208 storage self, + uint48 key, + uint208 value + ) internal returns (uint208 oldValue, uint208 newValue) { + return _insert(self._checkpoints, key, value); + } + + /** + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if + * there is none. + */ + function lowerLookup(Trace208 storage self, uint48 key) internal view returns (uint208) { + uint256 len = self._checkpoints.length; + uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); + return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + */ + function upperLookup(Trace208 storage self, uint48 key) internal view returns (uint208) { + uint256 len = self._checkpoints.length; + uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high + * keys). + */ + function upperLookupRecent(Trace208 storage self, uint48 key) internal view returns (uint208) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. + */ + function latest(Trace208 storage self) internal view returns (uint208) { + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value + * in the most recent checkpoint. + */ + function latestCheckpoint(Trace208 storage self) internal view returns (bool exists, uint48 _key, uint208 _value) { + uint256 pos = self._checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint208 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); + return (true, ckpt._key, ckpt._value); + } + } + + /** + * @dev Returns the number of checkpoint. + */ + function length(Trace208 storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace208 storage self, uint32 pos) internal view returns (Checkpoint208 memory) { + return self._checkpoints[pos]; + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, + * or by updating the last one. + */ + function _insert( + Checkpoint208[] storage self, + uint48 key, + uint208 value + ) private returns (uint208 oldValue, uint208 newValue) { + uint256 pos = self.length; + + if (pos > 0) { + Checkpoint208 storage last = _unsafeAccess(self, pos - 1); + uint48 lastKey = last._key; + uint208 lastValue = last._value; + + // Checkpoint keys must be non-decreasing. + if (lastKey > key) { + revert CheckpointUnorderedInsertion(); + } + + // Update or push new checkpoint + if (lastKey == key) { + last._value = value; + } else { + self.push(Checkpoint208({_key: key, _value: value})); + } + return (lastValue, value); + } else { + self.push(Checkpoint208({_key: key, _value: value})); + return (0, value); + } + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _upperBinaryLookup( + Checkpoint208[] storage self, + uint48 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key > key) { + high = mid; + } else { + low = mid + 1; + } + } + return high; + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _lowerBinaryLookup( + Checkpoint208[] storage self, + uint48 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess( + Checkpoint208[] storage self, + uint256 pos + ) private pure returns (Checkpoint208 storage result) { + assembly { + mstore(0, self.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } + + struct Trace160 { + Checkpoint160[] _checkpoints; + } + + struct Checkpoint160 { + uint96 _key; + uint160 _value; + } + + /** + * @dev Pushes a (`key`, `value`) pair into a Trace160 so that it is stored as the checkpoint. + * + * Returns previous value and new value. + * + * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint96).max` key set will disable the + * library. + */ + function push( + Trace160 storage self, + uint96 key, + uint160 value + ) internal returns (uint160 oldValue, uint160 newValue) { + return _insert(self._checkpoints, key, value); + } + + /** + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if + * there is none. + */ + function lowerLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len); + return pos == len ? 0 : _unsafeAccess(self._checkpoints, pos)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + */ + function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero + * if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high + * keys). + */ + function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. + */ + function latest(Trace160 storage self) internal view returns (uint160) { + uint256 pos = self._checkpoints.length; + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + + /** + * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value + * in the most recent checkpoint. + */ + function latestCheckpoint(Trace160 storage self) internal view returns (bool exists, uint96 _key, uint160 _value) { + uint256 pos = self._checkpoints.length; + if (pos == 0) { + return (false, 0, 0); + } else { + Checkpoint160 storage ckpt = _unsafeAccess(self._checkpoints, pos - 1); + return (true, ckpt._key, ckpt._value); + } + } + + /** + * @dev Returns the number of checkpoint. + */ + function length(Trace160 storage self) internal view returns (uint256) { + return self._checkpoints.length; + } + + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace160 storage self, uint32 pos) internal view returns (Checkpoint160 memory) { + return self._checkpoints[pos]; + } + + /** + * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, + * or by updating the last one. + */ + function _insert( + Checkpoint160[] storage self, + uint96 key, + uint160 value + ) private returns (uint160 oldValue, uint160 newValue) { + uint256 pos = self.length; + + if (pos > 0) { + Checkpoint160 storage last = _unsafeAccess(self, pos - 1); + uint96 lastKey = last._key; + uint160 lastValue = last._value; + + // Checkpoint keys must be non-decreasing. + if (lastKey > key) { + revert CheckpointUnorderedInsertion(); + } + + // Update or push new checkpoint + if (lastKey == key) { + last._value = value; + } else { + self.push(Checkpoint160({_key: key, _value: value})); + } + return (lastValue, value); + } else { + self.push(Checkpoint160({_key: key, _value: value})); + return (0, value); + } + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _upperBinaryLookup( + Checkpoint160[] storage self, + uint96 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key > key) { + high = mid; + } else { + low = mid + 1; + } + } + return high; + } + + /** + * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or `high` + * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive + * `high`. + * + * WARNING: `high` should not be greater than the array's length. + */ + function _lowerBinaryLookup( + Checkpoint160[] storage self, + uint96 key, + uint256 low, + uint256 high + ) private view returns (uint256) { + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(self, mid)._key < key) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess( + Checkpoint160[] storage self, + uint256 pos + ) private pure returns (Checkpoint160 storage result) { + assembly { + mstore(0, self.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } +} + + +// File @openzeppelin/contracts/utils/types/Time.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/types/Time.sol) + +pragma solidity ^0.8.20; + + +/** + * @dev This library provides helpers for manipulating time-related objects. + * + * It uses the following types: + * - `uint48` for timepoints + * - `uint32` for durations + * + * While the library doesn't provide specific types for timepoints and duration, it does provide: + * - a `Delay` type to represent duration that can be programmed to change value automatically at a given point + * - additional helper functions + */ +library Time { + using Time for *; + + /** + * @dev Get the block timestamp as a Timepoint. + */ + function timestamp() internal view returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + /** + * @dev Get the block number as a Timepoint. + */ + function blockNumber() internal view returns (uint48) { + return SafeCast.toUint48(block.number); + } + + // ==================================================== Delay ===================================================== + /** + * @dev A `Delay` is a uint32 duration that can be programmed to change value automatically at a given point in the + * future. The "effect" timepoint describes when the transitions happens from the "old" value to the "new" value. + * This allows updating the delay applied to some operation while keeping some guarantees. + * + * In particular, the {update} function guarantees that if the delay is reduced, the old delay still applies for + * some time. For example if the delay is currently 7 days to do an upgrade, the admin should not be able to set + * the delay to 0 and upgrade immediately. If the admin wants to reduce the delay, the old delay (7 days) should + * still apply for some time. + * + * + * The `Delay` type is 112 bits long, and packs the following: + * + * ``` + * | [uint48]: effect date (timepoint) + * | | [uint32]: value before (duration) + * ↓ ↓ ↓ [uint32]: value after (duration) + * 0xAAAAAAAAAAAABBBBBBBBCCCCCCCC + * ``` + * + * NOTE: The {get} and {withUpdate} functions operate using timestamps. Block number based delays are not currently + * supported. + */ + type Delay is uint112; + + /** + * @dev Wrap a duration into a Delay to add the one-step "update in the future" feature + */ + function toDelay(uint32 duration) internal pure returns (Delay) { + return Delay.wrap(duration); + } + + /** + * @dev Get the value at a given timepoint plus the pending value and effect timepoint if there is a scheduled + * change after this timepoint. If the effect timepoint is 0, then the pending value should not be considered. + */ + function _getFullAt( + Delay self, + uint48 timepoint + ) private pure returns (uint32 valueBefore, uint32 valueAfter, uint48 effect) { + (valueBefore, valueAfter, effect) = self.unpack(); + return effect <= timepoint ? (valueAfter, 0, 0) : (valueBefore, valueAfter, effect); + } + + /** + * @dev Get the current value plus the pending value and effect timepoint if there is a scheduled change. If the + * effect timepoint is 0, then the pending value should not be considered. + */ + function getFull(Delay self) internal view returns (uint32 valueBefore, uint32 valueAfter, uint48 effect) { + return _getFullAt(self, timestamp()); + } + + /** + * @dev Get the current value. + */ + function get(Delay self) internal view returns (uint32) { + (uint32 delay, , ) = self.getFull(); + return delay; + } + + /** + * @dev Update a Delay object so that it takes a new duration after a timepoint that is automatically computed to + * enforce the old delay at the moment of the update. Returns the updated Delay object and the timestamp when the + * new delay becomes effective. + */ + function withUpdate( + Delay self, + uint32 newValue, + uint32 minSetback + ) internal view returns (Delay updatedDelay, uint48 effect) { + uint32 value = self.get(); + uint32 setback = uint32(Math.max(minSetback, value > newValue ? value - newValue : 0)); + effect = timestamp() + setback; + return (pack(value, newValue, effect), effect); + } + + /** + * @dev Split a delay into its components: valueBefore, valueAfter and effect (transition timepoint). + */ + function unpack(Delay self) internal pure returns (uint32 valueBefore, uint32 valueAfter, uint48 effect) { + uint112 raw = Delay.unwrap(self); + + valueAfter = uint32(raw); + valueBefore = uint32(raw >> 32); + effect = uint48(raw >> 64); + + return (valueBefore, valueAfter, effect); + } + + /** + * @dev pack the components into a Delay object. + */ + function pack(uint32 valueBefore, uint32 valueAfter, uint48 effect) internal pure returns (Delay) { + return Delay.wrap((uint112(effect) << 64) | (uint112(valueBefore) << 32) | uint112(valueAfter)); + } +} + + +// File @openzeppelin/contracts/governance/utils/Votes.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (governance/utils/Votes.sol) +pragma solidity ^0.8.20; + + + + + + + + +/** + * @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be + * transferred, and provides a system of vote delegation, where an account can delegate its voting units to a sort of + * "representative" that will pool delegated voting units from different accounts and can then use it to vote in + * decisions. In fact, voting units _must_ be delegated in order to count as actual votes, and an account has to + * delegate those votes to itself if it wishes to participate in decisions and does not have a trusted representative. + * + * This contract is often combined with a token contract such that voting units correspond to token units. For an + * example, see {ERC721Votes}. + * + * The full history of delegate votes is tracked on-chain so that governance protocols can consider votes as distributed + * at a particular block number to protect against flash loans and double voting. The opt-in delegate system makes the + * cost of this history tracking optional. + * + * When using this module the derived contract must implement {_getVotingUnits} (for example, make it return + * {ERC721-balanceOf}), and can use {_transferVotingUnits} to track a change in the distribution of those units (in the + * previous example, it would be included in {ERC721-_update}). + */ +abstract contract Votes is Context, EIP712, Nonces, IERC5805 { + using Checkpoints for Checkpoints.Trace208; + + bytes32 private constant DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address account => address) private _delegatee; + + mapping(address delegatee => Checkpoints.Trace208) private _delegateCheckpoints; + + Checkpoints.Trace208 private _totalCheckpoints; + + /** + * @dev The clock was incorrectly modified. + */ + error ERC6372InconsistentClock(); + + /** + * @dev Lookup to future votes is not available. + */ + error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based + * checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. + */ + function clock() public view virtual returns (uint48) { + return Time.blockNumber(); + } + + /** + * @dev Machine-readable description of the clock as specified in ERC-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual returns (string memory) { + // Check that the clock was not modified + if (clock() != Time.blockNumber()) { + revert ERC6372InconsistentClock(); + } + return "mode=blocknumber&from=default"; + } + + /** + * @dev Validate that a timepoint is in the past, and return it as a uint48. + */ + function _validateTimepoint(uint256 timepoint) internal view returns (uint48) { + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) revert ERC5805FutureLookup(timepoint, currentTimepoint); + return SafeCast.toUint48(timepoint); + } + + /** + * @dev Returns the current amount of votes that `account` has. + */ + function getVotes(address account) public view virtual returns (uint256) { + return _delegateCheckpoints[account].latest(); + } + + /** + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + * + * Requirements: + * + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. + */ + function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { + return _delegateCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)); + } + + /** + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + * + * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. + * Votes that have not been delegated are still part of total supply, even though they would not participate in a + * vote. + * + * Requirements: + * + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. + */ + function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { + return _totalCheckpoints.upperLookupRecent(_validateTimepoint(timepoint)); + } + + /** + * @dev Returns the current total supply of votes. + */ + function _getTotalSupply() internal view virtual returns (uint256) { + return _totalCheckpoints.latest(); + } + + /** + * @dev Returns the delegate that `account` has chosen. + */ + function delegates(address account) public view virtual returns (address) { + return _delegatee[account]; + } + + /** + * @dev Delegates votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + address account = _msgSender(); + _delegate(account, delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee`. + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (block.timestamp > expiry) { + revert VotesExpiredSignature(expiry); + } + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + _useCheckedNonce(signer, nonce); + _delegate(signer, delegatee); + } + + /** + * @dev Delegate all of `account`'s voting units to `delegatee`. + * + * Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}. + */ + function _delegate(address account, address delegatee) internal virtual { + address oldDelegate = delegates(account); + _delegatee[account] = delegatee; + + emit DelegateChanged(account, oldDelegate, delegatee); + _moveDelegateVotes(oldDelegate, delegatee, _getVotingUnits(account)); + } + + /** + * @dev Transfers, mints, or burns voting units. To register a mint, `from` should be zero. To register a burn, `to` + * should be zero. Total supply of voting units will be adjusted with mints and burns. + */ + function _transferVotingUnits(address from, address to, uint256 amount) internal virtual { + if (from == address(0)) { + _push(_totalCheckpoints, _add, SafeCast.toUint208(amount)); + } + if (to == address(0)) { + _push(_totalCheckpoints, _subtract, SafeCast.toUint208(amount)); + } + _moveDelegateVotes(delegates(from), delegates(to), amount); + } + + /** + * @dev Moves delegated votes from one delegate to another. + */ + function _moveDelegateVotes(address from, address to, uint256 amount) internal virtual { + if (from != to && amount > 0) { + if (from != address(0)) { + (uint256 oldValue, uint256 newValue) = _push( + _delegateCheckpoints[from], + _subtract, + SafeCast.toUint208(amount) + ); + emit DelegateVotesChanged(from, oldValue, newValue); + } + if (to != address(0)) { + (uint256 oldValue, uint256 newValue) = _push( + _delegateCheckpoints[to], + _add, + SafeCast.toUint208(amount) + ); + emit DelegateVotesChanged(to, oldValue, newValue); + } + } + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function _numCheckpoints(address account) internal view virtual returns (uint32) { + return SafeCast.toUint32(_delegateCheckpoints[account].length()); + } + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function _checkpoints( + address account, + uint32 pos + ) internal view virtual returns (Checkpoints.Checkpoint208 memory) { + return _delegateCheckpoints[account].at(pos); + } + + function _push( + Checkpoints.Trace208 storage store, + function(uint208, uint208) view returns (uint208) op, + uint208 delta + ) private returns (uint208 oldValue, uint208 newValue) { + return store.push(clock(), op(store.latest(), delta)); + } + + function _add(uint208 a, uint208 b) private pure returns (uint208) { + return a + b; + } + + function _subtract(uint208 a, uint208 b) private pure returns (uint208) { + return a - b; + } + + /** + * @dev Must return the voting units held by an account. + */ + function _getVotingUnits(address) internal view virtual returns (uint256); +} + + +// File @openzeppelin/contracts/interfaces/draft-IERC6093.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC6093.sol) +pragma solidity ^0.8.20; + +/** + * @dev Standard ERC-20 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens. + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC-721 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens. + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC-1155 Errors + * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens. + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + * @param tokenId Identifier number of a token. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error ERC1155MissingApprovalForAll(address operator, address owner); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC1155InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1155InvalidOperator(address operator); + + /** + * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. + * Used in batch transfers. + * @param idsLength Length of the array of token identifiers + * @param valuesLength Length of the array of token amounts + */ + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} + + +// File @openzeppelin/contracts/token/ERC20/IERC20.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} + + +// File @openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface for the optional metadata functions from the ERC-20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} + + +// File @openzeppelin/contracts/token/ERC20/ERC20.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.20; + + + + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * The default value of {decimals} is 18. To change this, you should override + * this function so it returns a different value. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC-20 + * applications. + */ +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { + mapping(address account => uint256) private _balances; + + mapping(address account => mapping(address spender => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + + /** + * @dev Sets the values for {name} and {symbol}. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + constructor(string memory name_, string memory symbol_) { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the default value returned by this function, unless + * it's overridden. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `value`. + */ + function transfer(address to, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, value); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 value) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, value); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Skips emitting an {Approval} event indicating an allowance update. This is not + * required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve]. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `value`. + * - the caller must have allowance for ``from``'s tokens of at least + * `value`. + */ + function transferFrom(address from, address to, uint256 value) public virtual returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _transfer(from, to, value); + return true; + } + + /** + * @dev Moves a `value` amount of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 value) internal { + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(from, to, value); + } + + /** + * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 value) internal virtual { + if (from == address(0)) { + // Overflow check required: The rest of the code assumes that totalSupply never overflows + _totalSupply += value; + } else { + uint256 fromBalance = _balances[from]; + if (fromBalance < value) { + revert ERC20InsufficientBalance(from, fromBalance, value); + } + unchecked { + // Overflow not possible: value <= fromBalance <= totalSupply. + _balances[from] = fromBalance - value; + } + } + + if (to == address(0)) { + unchecked { + // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. + _totalSupply -= value; + } + } else { + unchecked { + // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. + _balances[to] += value; + } + } + + emit Transfer(from, to, value); + } + + /** + * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } + _update(address(0), account, value); + } + + /** + * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address account, uint256 value) internal { + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } + _update(account, address(0), value); + } + + /** + * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + * + * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. + */ + function _approve(address owner, address spender, uint256 value) internal { + _approve(owner, spender, value, true); + } + + /** + * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to + * true using the following override: + * + * ```solidity + * function _approve(address owner, address spender, uint256 value, bool) internal virtual override { + * super._approve(owner, spender, value, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } + _allowances[owner][spender] = value; + if (emitEvent) { + emit Approval(owner, spender, value); + } + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `value`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 value) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance < type(uint256).max) { + if (currentAllowance < value) { + revert ERC20InsufficientAllowance(spender, currentAllowance, value); + } + unchecked { + _approve(owner, spender, currentAllowance - value, false); + } + } + } +} + + +// File @openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Permit.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Interface of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} + + +// File @openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Permit.sol) + +pragma solidity ^0.8.20; + + + + + +/** + * @dev Implementation of the ERC-20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[ERC-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC-20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + */ +abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { + bytes32 private constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev Permit deadline has expired. + */ + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @dev Mismatched signature. + */ + error ERC2612InvalidSigner(address signer, address owner); + + /** + * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. + * + * It's a good idea to use the same `name` that is defined as the ERC-20 token name. + */ + constructor(string memory name) EIP712(name, "1") {} + + /** + * @inheritdoc IERC20Permit + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSA.recover(hash, v, r, s); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } + + _approve(owner, spender, value); + } + + /** + * @inheritdoc IERC20Permit + */ + function nonces(address owner) public view virtual override(IERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } + + /** + * @inheritdoc IERC20Permit + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { + return _domainSeparatorV4(); + } +} + + +// File @openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Votes.sol) + +pragma solidity ^0.8.20; + + + +/** + * @dev Extension of ERC-20 to support Compound-like voting and delegation. This version is more generic than Compound's, + * and supports token supply up to 2^208^ - 1, while COMP is limited to 2^96^ - 1. + * + * NOTE: This contract does not provide interface compatibility with Compound's COMP token. + * + * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either + * by calling the {Votes-delegate} function directly, or by providing a signature to be used with {Votes-delegateBySig}. Voting + * power can be queried through the public accessors {Votes-getVotes} and {Votes-getPastVotes}. + * + * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it + * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. + */ +abstract contract ERC20Votes is ERC20, Votes { + /** + * @dev Total supply cap has been exceeded, introducing a risk of votes overflowing. + */ + error ERC20ExceededSafeSupply(uint256 increasedSupply, uint256 cap); + + /** + * @dev Maximum token supply. Defaults to `type(uint208).max` (2^208^ - 1). + * + * This maximum is enforced in {_update}. It limits the total supply of the token, which is otherwise a uint256, + * so that checkpoints can be stored in the Trace208 structure used by {Votes}. Increasing this value will not + * remove the underlying limitation, and will cause {_update} to fail because of a math overflow in + * {Votes-_transferVotingUnits}. An override could be used to further restrict the total supply (to a lower value) if + * additional logic requires it. When resolving override conflicts on this function, the minimum should be + * returned. + */ + function _maxSupply() internal view virtual returns (uint256) { + return type(uint208).max; + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {IVotes-DelegateVotesChanged} event. + */ + function _update(address from, address to, uint256 value) internal virtual override { + super._update(from, to, value); + if (from == address(0)) { + uint256 supply = totalSupply(); + uint256 cap = _maxSupply(); + if (supply > cap) { + revert ERC20ExceededSafeSupply(supply, cap); + } + } + _transferVotingUnits(from, to, value); + } + + /** + * @dev Returns the voting units of an `account`. + * + * WARNING: Overriding this function may compromise the internal vote accounting. + * `ERC20Votes` assumes tokens map to voting units 1:1 and this is not easy to change. + */ + function _getVotingUnits(address account) internal view virtual override returns (uint256) { + return balanceOf(account); + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return _numCheckpoints(account); + } + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint208 memory) { + return _checkpoints(account, pos); + } +} + + +// File @openzeppelin/contracts/utils/ReentrancyGuard.sol@v5.2.0 + +// Original license: SPDX_License_Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at, + * consider using {ReentrancyGuardTransient} instead. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant NOT_ENTERED = 1; + uint256 private constant ENTERED = 2; + + uint256 private _status; + + /** + * @dev Unauthorized reentrant call. + */ + error ReentrancyGuardReentrantCall(); + + constructor() { + _status = NOT_ENTERED; + } + + /** + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + function _nonReentrantBefore() private { + // On the first call to nonReentrant, _status will be NOT_ENTERED + if (_status == ENTERED) { + revert ReentrancyGuardReentrantCall(); + } + + // Any calls to nonReentrant after this point will fail + _status = ENTERED; + } + + function _nonReentrantAfter() private { + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = NOT_ENTERED; + } + + /** + * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a + * `nonReentrant` function in the call stack. + */ + function _reentrancyGuardEntered() internal view returns (bool) { + return _status == ENTERED; + } +} + + +// File contracts/DLE.sol + +// Original license: SPDX_License_Identifier: PROPRIETARY +// Copyright (c) 2024-2025 Тарабанов Александр Викторович +// All rights reserved. +// For licensing inquiries: info@hb3-accelerator.com +pragma solidity ^0.8.20; + + + + + +interface IERC1271 { + function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue); +} + +interface IMultichainMetadata { + function getMultichainInfo() external view returns (uint256[] memory supportedChainIds, uint256 defaultVotingChain); + function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses); +} + +// DLE (Digital Legal Entity) - основной контракт с модульной архитектурой +contract DLE is ERC20, ERC20Permit, ERC20Votes, ReentrancyGuard, IMultichainMetadata { + using ECDSA for bytes32; + struct DLEInfo { + string name; + string symbol; + string location; + string coordinates; + uint256 jurisdiction; + string[] okvedCodes; + uint256 kpp; + uint256 creationTimestamp; + bool isActive; + } + + struct DLEConfig { + string name; + string symbol; + string location; + string coordinates; + uint256 jurisdiction; + string[] okvedCodes; + uint256 kpp; + uint256 quorumPercentage; + address[] initialPartners; + uint256[] initialAmounts; + uint256[] supportedChainIds; // Поддерживаемые цепочки + } + + struct Proposal { + uint256 id; + string description; + uint256 forVotes; + uint256 againstVotes; + bool executed; + bool canceled; + uint256 deadline; // конец периода голосования (sec) + address initiator; + bytes operation; // операция для исполнения + uint256 governanceChainId; // сеть голосования (Single-Chain Governance) + uint256[] targetChains; // целевые сети для исполнения + uint256 snapshotTimepoint; // блок/временная точка для getPastVotes + mapping(address => bool) hasVoted; + } + + + + // Основные настройки + DLEInfo public dleInfo; + uint256 public quorumPercentage; + uint256 public proposalCounter; + // Удален currentChainId - теперь используется block.chainid для проверок + // Публичный URI логотипа токена/организации (можно установить при деплое через инициализатор) + string public logoURI; + + // Модули + mapping(bytes32 => address) public modules; + mapping(bytes32 => bool) public activeModules; + address public immutable initializer; // Адрес, имеющий право на однократную инициализацию логотипа + + // Предложения + mapping(uint256 => Proposal) public proposals; + uint256[] public allProposalIds; + + // Мульти-чейн + mapping(uint256 => bool) public supportedChains; + uint256[] public supportedChainIds; + + // События + event DLEInitialized( + string name, + string symbol, + string location, + string coordinates, + uint256 jurisdiction, + string[] okvedCodes, + uint256 kpp, + address tokenAddress, + uint256[] supportedChainIds + ); + event InitialTokensDistributed(address[] partners, uint256[] amounts); + event ProposalCreated(uint256 proposalId, address initiator, string description); + event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower); + 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); + event ChainAdded(uint256 chainId); + event ChainRemoved(uint256 chainId); + event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp); + event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage); + event TokensTransferredByGovernance(address indexed recipient, uint256 amount); + + event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration); + event LogoURIUpdated(string oldURI, string newURI); + + // EIP712 typehash для подписи одобрения исполнения предложения + bytes32 private constant EXECUTION_APPROVAL_TYPEHASH = keccak256( + "ExecutionApproval(uint256 proposalId,bytes32 operationHash,uint256 chainId,uint256 snapshotTimepoint)" + ); + // Custom errors (reduce bytecode size) + error ErrZeroAddress(); + error ErrArrayMismatch(); + error ErrNoPartners(); + error ErrZeroAmount(); + error ErrOnlyInitializer(); + error ErrLogoAlreadySet(); + error ErrNotHolder(); + error ErrTooShort(); + error ErrTooLong(); + error ErrBadChain(); + error ErrProposalMissing(); + error ErrProposalEnded(); + error ErrProposalExecuted(); + error ErrAlreadyVoted(); + error ErrWrongChain(); + error ErrUnsupportedChain(); + error ErrNoPower(); + error ErrNotReady(); + error ErrNotInitiator(); + error ErrLowPower(); + error ErrBadTarget(); + error ErrBadSig1271(); + error ErrBadSig(); + error ErrDuplicateSigner(); + error ErrNoSigners(); + error ErrSigLengthMismatch(); + error ErrInvalidOperation(); + error ErrNameEmpty(); + error ErrSymbolEmpty(); + error ErrLocationEmpty(); + error ErrBadJurisdiction(); + error ErrBadKPP(); + error ErrBadQuorum(); + error ErrChainAlreadySupported(); + error ErrChainNotSupported(); + error ErrCannotRemoveCurrentChain(); + error ErrTransfersDisabled(); + error ErrApprovalsDisabled(); + error ErrProposalCanceled(); + + // Константы безопасности (можно изменять через governance) + uint256 public maxVotingDuration = 30 days; // Максимальное время голосования + uint256 public minVotingDuration = 1 hours; // Минимальное время голосования + // Удалён буфер ограничения голосования в последние минуты перед дедлайном + + constructor( + DLEConfig memory config, + address _initializer + ) ERC20(config.name, config.symbol) ERC20Permit(config.name) { + if (_initializer == address(0)) revert ErrZeroAddress(); + initializer = _initializer; + dleInfo = DLEInfo({ + name: config.name, + symbol: config.symbol, + location: config.location, + coordinates: config.coordinates, + jurisdiction: config.jurisdiction, + okvedCodes: config.okvedCodes, + kpp: config.kpp, + creationTimestamp: block.timestamp, + isActive: true + }); + + quorumPercentage = config.quorumPercentage; + + // Настраиваем поддерживаемые цепочки + for (uint256 i = 0; i < config.supportedChainIds.length; i++) { + supportedChains[config.supportedChainIds[i]] = true; + supportedChainIds.push(config.supportedChainIds[i]); + } + + // Распределяем начальные токены партнерам + if (config.initialPartners.length != config.initialAmounts.length) revert ErrArrayMismatch(); + if (config.initialPartners.length == 0) revert ErrNoPartners(); + + for (uint256 i = 0; i < config.initialPartners.length; i++) { + address partner = config.initialPartners[i]; + uint256 amount = config.initialAmounts[i]; + if (partner == address(0)) revert ErrZeroAddress(); + if (amount == 0) revert ErrZeroAmount(); + _mint(partner, amount); + // Авто-делегирование голосов себе, чтобы getPastVotes работал без действия пользователя + _delegate(partner, partner); + } + + emit InitialTokensDistributed(config.initialPartners, config.initialAmounts); + emit DLEInitialized( + config.name, + config.symbol, + config.location, + config.coordinates, + config.jurisdiction, + config.okvedCodes, + config.kpp, + address(this), + config.supportedChainIds + ); + } + + // Одноразовая инициализация URI логотипа + function initializeLogoURI(string calldata _logoURI) external { + if (msg.sender != initializer) revert ErrOnlyInitializer(); + if (bytes(logoURI).length != 0) revert ErrLogoAlreadySet(); + string memory old = logoURI; + logoURI = _logoURI; + emit LogoURIUpdated(old, _logoURI); + } + + // Создать предложение с выбором цепочки для кворума + 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 + ); + } + + function _createProposalInternal( + string memory _description, + uint256 _duration, + bytes memory _operation, + uint256 _governanceChainId, + uint256[] memory _targetChains, + address _initiator + ) internal returns (uint256) { + uint256 proposalId = proposalCounter++; + Proposal storage proposal = proposals[proposalId]; + + proposal.id = proposalId; + proposal.description = _description; + proposal.forVotes = 0; + proposal.againstVotes = 0; + proposal.executed = false; + proposal.deadline = block.timestamp + _duration; + proposal.initiator = _initiator; + proposal.operation = _operation; + proposal.governanceChainId = _governanceChainId; + + // Снимок голосов: используем прошлую точку времени, чтобы getPastVotes был валиден в текущем блоке + uint256 nowClock = clock(); + proposal.snapshotTimepoint = nowClock == 0 ? 0 : nowClock - 1; + + // запись целевых сетей + for (uint256 i = 0; i < _targetChains.length; i++) { + if (!supportedChains[_targetChains[i]]) revert ErrBadTarget(); + proposal.targetChains.push(_targetChains[i]); + } + + allProposalIds.push(proposalId); + emit ProposalCreated(proposalId, _initiator, _description); + emit ProposalGovernanceChainSet(proposalId, _governanceChainId); + emit ProposalTargetsSet(proposalId, _targetChains); + return proposalId; + } + + // Голосовать за предложение + function vote(uint256 _proposalId, bool _support) external nonReentrant { + Proposal storage proposal = proposals[_proposalId]; + if (proposal.id != _proposalId) revert ErrProposalMissing(); + if (block.timestamp >= proposal.deadline) revert ErrProposalEnded(); + if (proposal.executed) revert ErrProposalExecuted(); + if (proposal.canceled) revert ErrProposalCanceled(); + if (proposal.hasVoted[msg.sender]) revert ErrAlreadyVoted(); + // Проверяем, что текущая сеть поддерживается + if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); + + uint256 votingPower = getPastVotes(msg.sender, proposal.snapshotTimepoint); + if (votingPower == 0) revert ErrNoPower(); + proposal.hasVoted[msg.sender] = true; + + if (_support) { + proposal.forVotes += votingPower; + } else { + proposal.againstVotes += votingPower; + } + + emit ProposalVoted(_proposalId, msg.sender, _support, votingPower); + } + + function checkProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached) { + Proposal storage proposal = proposals[_proposalId]; + if (proposal.id != _proposalId) revert ErrProposalMissing(); + + uint256 totalVotes = proposal.forVotes + proposal.againstVotes; + // Используем снапшот totalSupply на момент начала голосования + uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); + uint256 quorumRequired = (pastSupply * quorumPercentage) / 100; + + quorumReached = totalVotes >= quorumRequired; + passed = quorumReached && proposal.forVotes > proposal.againstVotes; + + return (passed, quorumReached); + } + + + function executeProposal(uint256 _proposalId) external { + Proposal storage proposal = proposals[_proposalId]; + if (proposal.id != _proposalId) revert ErrProposalMissing(); + if (proposal.executed) revert ErrProposalExecuted(); + if (proposal.canceled) revert ErrProposalCanceled(); + // Проверяем, что текущая сеть поддерживается + if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); + + (bool passed, bool quorumReached) = checkProposalResult(_proposalId); + + // Предложение можно выполнить если: + // 1. Дедлайн истек ИЛИ кворум достигнут + if (!(block.timestamp >= proposal.deadline || quorumReached)) revert ErrNotReady(); + if (!(passed && quorumReached)) revert ErrNotReady(); + + proposal.executed = true; + + // Исполняем операцию + _executeOperation(proposal.operation); + + emit ProposalExecuted(_proposalId, proposal.operation); + } + + + function cancelProposal(uint256 _proposalId, string calldata reason) external { + Proposal storage proposal = proposals[_proposalId]; + if (proposal.id != _proposalId) revert ErrProposalMissing(); + if (proposal.executed) revert ErrProposalExecuted(); + if (block.timestamp + 900 >= proposal.deadline) revert ErrProposalEnded(); + if (msg.sender != proposal.initiator) revert ErrNotInitiator(); + uint256 vp = getPastVotes(msg.sender, proposal.snapshotTimepoint); + uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); + if (vp * 10 < pastSupply) revert ErrLowPower(); + + proposal.canceled = true; + emit ProposalCancelled(_proposalId, reason); + } + + // УДАЛЕНО: syncExecutionFromChain с MerkleProof — небезопасно без доверенного моста + function executeProposalBySignatures( + uint256 _proposalId, + address[] calldata signers, + bytes[] calldata signatures + ) external nonReentrant { + Proposal storage proposal = proposals[_proposalId]; + if (proposal.id != _proposalId) revert ErrProposalMissing(); + if (proposal.executed) revert ErrProposalExecuted(); + if (proposal.canceled) revert ErrProposalCanceled(); + // Проверяем, что текущая сеть поддерживается + if (!supportedChains[block.chainid]) revert ErrUnsupportedChain(); + // Проверяем, что текущая сеть является целевой для предложения + if (!_isTargetChain(proposal, block.chainid)) revert ErrBadTarget(); + + if (signers.length != signatures.length) revert ErrSigLengthMismatch(); + if (signers.length == 0) revert ErrNoSigners(); + // Все держатели токенов имеют право голосовать + + bytes32 opHash = keccak256(proposal.operation); + bytes32 structHash = keccak256(abi.encode( + EXECUTION_APPROVAL_TYPEHASH, + _proposalId, + opHash, + block.chainid, + proposal.snapshotTimepoint + )); + bytes32 digest = _hashTypedDataV4(structHash); + + uint256 votesFor = 0; + + for (uint256 i = 0; i < signers.length; i++) { + address signer = signers[i]; + if (signer.code.length > 0) { + // Контрактный кошелёк: проверяем подпись по EIP-1271 + try IERC1271(signer).isValidSignature(digest, signatures[i]) returns (bytes4 magic) { + if (magic != 0x1626ba7e) revert ErrBadSig1271(); + } catch { + revert ErrBadSig1271(); + } + } else { + // EOA подпись через ECDSA + address recovered = ECDSA.recover(digest, signatures[i]); + if (recovered != signer) revert ErrBadSig(); + } + + for (uint256 j = 0; j < i; j++) { + if (signers[j] == signer) revert ErrDuplicateSigner(); + } + + uint256 vp = getPastVotes(signer, proposal.snapshotTimepoint); + if (vp == 0) revert ErrNoPower(); + votesFor += vp; + } + + uint256 pastSupply = getPastTotalSupply(proposal.snapshotTimepoint); + uint256 quorumRequired = (pastSupply * quorumPercentage) / 100; + if (votesFor < quorumRequired) revert ErrNoPower(); + + proposal.executed = true; + _executeOperation(proposal.operation); + emit ProposalExecuted(_proposalId, proposal.operation); + emit ProposalExecutionApprovedInChain(_proposalId, block.chainid); + + } + + /** + * @dev Получить количество поддерживаемых цепочек + */ + function getSupportedChainCount() public view returns (uint256) { + return supportedChainIds.length; + } + + /** + * @dev Получить ID поддерживаемой цепочки по индексу + * @param _index Индекс цепочки + */ + function getSupportedChainId(uint256 _index) public view returns (uint256) { + require(_index < supportedChainIds.length, "Invalid chain index"); + return supportedChainIds[_index]; + } + + /** + * @dev Добавить поддерживаемую цепочку (только для владельцев токенов) + * @param _chainId ID цепочки + */ + // Управление списком сетей теперь выполняется только через предложения + function _addSupportedChain(uint256 _chainId) internal { + require(!supportedChains[_chainId], "Chain already supported"); + require(_chainId != block.chainid, "Cannot add current chain"); + supportedChains[_chainId] = true; + supportedChainIds.push(_chainId); + emit ChainAdded(_chainId); + } + + /** + * @dev Удалить поддерживаемую цепочку (только для владельцев токенов) + * @param _chainId ID цепочки + */ + function _removeSupportedChain(uint256 _chainId) internal { + require(supportedChains[_chainId], "Chain not supported"); + require(_chainId != block.chainid, "Cannot remove current chain"); + supportedChains[_chainId] = false; + // Удаляем из массива + for (uint256 i = 0; i < supportedChainIds.length; i++) { + if (supportedChainIds[i] == _chainId) { + supportedChainIds[i] = supportedChainIds[supportedChainIds.length - 1]; + supportedChainIds.pop(); + break; + } + } + emit ChainRemoved(_chainId); + } + + + /** + * @dev Исполнить операцию + * @param _operation Операция для исполнения + */ + function _executeOperation(bytes memory _operation) internal { + if (_operation.length < 4) revert ErrInvalidOperation(); + + // Декодируем операцию из formата abi.encodeWithSelector + bytes4 selector; + bytes memory data; + + // Извлекаем селектор (первые 4 байта) + assembly { + selector := mload(add(_operation, 0x20)) + } + + // Извлекаем данные (все после первых 4 байтов) + if (_operation.length > 4) { + data = new bytes(_operation.length - 4); + for (uint256 i = 0; i < data.length; i++) { + data[i] = _operation[i + 4]; + } + } else { + data = new bytes(0); + } + + if (selector == bytes4(keccak256("_addModule(bytes32,address)"))) { + // Операция добавления модуля + (bytes32 moduleId, address moduleAddress) = abi.decode(data, (bytes32, address)); + _addModule(moduleId, moduleAddress); + } else if (selector == bytes4(keccak256("_removeModule(bytes32)"))) { + // Операция удаления модуля + (bytes32 moduleId) = abi.decode(data, (bytes32)); + _removeModule(moduleId); + } else if (selector == bytes4(keccak256("_addSupportedChain(uint256)"))) { + (uint256 chainIdToAdd) = abi.decode(data, (uint256)); + _addSupportedChain(chainIdToAdd); + } 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("_updateVotingDurations(uint256,uint256)"))) { + // Операция обновления времени голосования + (uint256 newMinDuration, uint256 newMaxDuration) = abi.decode(data, (uint256, uint256)); + _updateVotingDurations(newMinDuration, newMaxDuration); + } else if (selector == bytes4(keccak256("_setLogoURI(string)"))) { + // Обновление логотипа через governance + (string memory newLogo) = abi.decode(data, (string)); + _setLogoURI(newLogo); + } else if (selector == bytes4(keccak256("_updateQuorumPercentage(uint256)"))) { + // Операция обновления процента кворума + (uint256 newQuorumPercentage) = abi.decode(data, (uint256)); + _updateQuorumPercentage(newQuorumPercentage); + } else if (selector == bytes4(keccak256("_updateDLEInfo(string,string,string,string,uint256,string[],uint256)"))) { + // Операция обновления информации DLE + (string memory name, string memory symbol, string memory location, string memory coordinates, uint256 jurisdiction, string[] memory okvedCodes, uint256 kpp) = abi.decode(data, (string, string, string, string, uint256, string[], uint256)); + _updateDLEInfo(name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp); + } else if (selector == bytes4(keccak256("offchainAction(bytes32,string,bytes32)"))) { + // Оффчейн операция для приложения: идентификатор, тип, хеш полезной нагрузки + // (bytes32 actionId, string memory kind, bytes32 payloadHash) = abi.decode(data, (bytes32, string, bytes32)); + // Ончейн-побочных эффектов нет. Факт решения фиксируется событием ProposalExecuted. + } else { + revert ErrInvalidOperation(); + } + } + + /** + * @dev Обновить информацию DLE + * @param _name Новое название + * @param _symbol Новый символ + * @param _location Новое местонахождение + * @param _coordinates Новые координаты + * @param _jurisdiction Новая юрисдикция + * @param _okvedCodes Новые коды ОКВЭД + * @param _kpp Новый КПП + */ + function _updateDLEInfo( + string memory _name, + string memory _symbol, + string memory _location, + string memory _coordinates, + uint256 _jurisdiction, + string[] memory _okvedCodes, + uint256 _kpp + ) internal { + if (bytes(_name).length == 0) revert ErrNameEmpty(); + if (bytes(_symbol).length == 0) revert ErrSymbolEmpty(); + if (bytes(_location).length == 0) revert ErrLocationEmpty(); + if (_jurisdiction == 0) revert ErrBadJurisdiction(); + if (_kpp == 0) revert ErrBadKPP(); + + dleInfo.name = _name; + dleInfo.symbol = _symbol; + dleInfo.location = _location; + dleInfo.coordinates = _coordinates; + dleInfo.jurisdiction = _jurisdiction; + dleInfo.okvedCodes = _okvedCodes; + dleInfo.kpp = _kpp; + + emit DLEInfoUpdated(_name, _symbol, _location, _coordinates, _jurisdiction, _okvedCodes, _kpp); + } + + /** + * @dev Обновить процент кворума + * @param _newQuorumPercentage Новый процент кворума + */ + function _updateQuorumPercentage(uint256 _newQuorumPercentage) internal { + if (!(_newQuorumPercentage > 0 && _newQuorumPercentage <= 100)) revert ErrBadQuorum(); + + uint256 oldQuorumPercentage = quorumPercentage; + quorumPercentage = _newQuorumPercentage; + + emit QuorumPercentageUpdated(oldQuorumPercentage, _newQuorumPercentage); + } + + + /** + * @dev Перевести токены через governance (от имени DLE) + * @param _recipient Адрес получателя + * @param _amount Количество токенов для перевода + */ + function _transferTokens(address _recipient, uint256 _amount) internal { + if (_recipient == address(0)) revert ErrZeroAddress(); + if (_amount == 0) revert ErrZeroAmount(); + require(balanceOf(address(this)) >= _amount, "Insufficient DLE balance"); + + // Переводим токены от имени DLE (address(this)) + _transfer(address(this), _recipient, _amount); + + emit TokensTransferredByGovernance(_recipient, _amount); + } + + /** + * @dev Обновить время голосования (только через governance) + * @param _newMinDuration Новое минимальное время голосования + * @param _newMaxDuration Новое максимальное время голосования + */ + function _updateVotingDurations(uint256 _newMinDuration, uint256 _newMaxDuration) internal { + if (_newMinDuration == 0) revert ErrTooShort(); + if (!(_newMaxDuration > _newMinDuration)) revert ErrTooLong(); + if (_newMinDuration < 10 minutes) revert ErrTooShort(); + if (_newMaxDuration > 365 days) revert ErrTooLong(); + + uint256 oldMinDuration = minVotingDuration; + uint256 oldMaxDuration = maxVotingDuration; + + minVotingDuration = _newMinDuration; + maxVotingDuration = _newMaxDuration; + + emit VotingDurationsUpdated(oldMinDuration, _newMinDuration, oldMaxDuration, _newMaxDuration); + } + + /** + * @dev Внутреннее обновление URI логотипа (только через governance). + */ + function _setLogoURI(string memory _logoURI) internal { + string memory old = logoURI; + logoURI = _logoURI; + emit LogoURIUpdated(old, _logoURI); + } + + + + + /** + * @dev Создать предложение о добавлении модуля + * @param _description Описание предложения + * @param _duration Длительность голосования в секундах + * @param _moduleId ID модуля + * @param _moduleAddress Адрес модуля + * @param _chainId ID цепочки для голосования + */ + function createAddModuleProposal( + string memory _description, + uint256 _duration, + bytes32 _moduleId, + address _moduleAddress, + uint256 _chainId + ) external returns (uint256) { + if (!supportedChains[_chainId]) revert ErrChainNotSupported(); + if (_moduleAddress == address(0)) revert ErrZeroAddress(); + if (activeModules[_moduleId]) revert ErrProposalExecuted(); + if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); + + // Операция добавления модуля + bytes memory operation = abi.encodeWithSelector( + bytes4(keccak256("_addModule(bytes32,address)")), + _moduleId, + _moduleAddress + ); + + // Целевые сети: по умолчанию все поддерживаемые сети + uint256[] memory targets = new uint256[](supportedChainIds.length); + for (uint256 i = 0; i < supportedChainIds.length; i++) { + targets[i] = supportedChainIds[i]; + } + + // Таймлок больше не используется в ядре; модуль Timelock будет добавлен отдельно + return _createProposalInternal( + _description, + _duration, + operation, + _chainId, + targets, + msg.sender + ); + } + + /** + * @dev Создать предложение об удалении модуля + * @param _description Описание предложения + * @param _duration Длительность голосования в секундах + * @param _moduleId ID модуля + * @param _chainId ID цепочки для голосования + */ + function createRemoveModuleProposal( + string memory _description, + uint256 _duration, + bytes32 _moduleId, + uint256 _chainId + ) external returns (uint256) { + if (!supportedChains[_chainId]) revert ErrChainNotSupported(); + if (!activeModules[_moduleId]) revert ErrProposalMissing(); + if (balanceOf(msg.sender) == 0) revert ErrNotHolder(); + + // Операция удаления модуля + bytes memory operation = abi.encodeWithSelector( + bytes4(keccak256("_removeModule(bytes32)")), + _moduleId + ); + + // Целевые сети: по умолчанию все поддерживаемые сети + uint256[] memory targets = new uint256[](supportedChainIds.length); + for (uint256 i = 0; i < supportedChainIds.length; i++) { + targets[i] = supportedChainIds[i]; + } + + // Таймлок больше не используется в ядре; модуль Timelock будет добавлен отдельно + return _createProposalInternal( + _description, + _duration, + operation, + _chainId, + targets, + msg.sender + ); + } + + // Treasury операции перенесены в TreasuryModule для экономии байт-кода + + /** + * @dev Добавить модуль (внутренняя функция, вызывается через кворум) + * @param _moduleId ID модуля + * @param _moduleAddress Адрес модуля + */ + function _addModule(bytes32 _moduleId, address _moduleAddress) internal { + if (_moduleAddress == address(0)) revert ErrZeroAddress(); + if (activeModules[_moduleId]) revert ErrProposalExecuted(); + + modules[_moduleId] = _moduleAddress; + activeModules[_moduleId] = true; + + emit ModuleAdded(_moduleId, _moduleAddress); + } + + /** + * @dev Удалить модуль (внутренняя функция, вызывается через кворум) + * @param _moduleId ID модуля + */ + function _removeModule(bytes32 _moduleId) internal { + if (!activeModules[_moduleId]) revert ErrProposalMissing(); + + delete modules[_moduleId]; + activeModules[_moduleId] = false; + + emit ModuleRemoved(_moduleId); + } + + /** + * @dev Получить информацию о DLE + */ + function getDLEInfo() external view returns (DLEInfo memory) { + return dleInfo; + } + + /** + * @dev Проверить, активен ли модуль + * @param _moduleId ID модуля + */ + function isModuleActive(bytes32 _moduleId) external view returns (bool) { + return activeModules[_moduleId]; + } + + /** + * @dev Получить адрес модуля + * @param _moduleId ID модуля + */ + function getModuleAddress(bytes32 _moduleId) external view returns (address) { + return modules[_moduleId]; + } + + /** + * @dev Проверить, поддерживается ли цепочка + * @param _chainId ID цепочки + */ + function isChainSupported(uint256 _chainId) external view returns (bool) { + return supportedChains[_chainId]; + } + + /** + * @dev Получить текущий ID цепочки (теперь используется block.chainid) + */ + function getCurrentChainId() external view returns (uint256) { + return block.chainid; + } + + /** + * @dev Получить URI логотипа токена (стандартная функция для блокчейн-сканеров) + * @return URI логотипа или пустую строку если не установлен + */ + function tokenURI() external view returns (string memory) { + return logoURI; + } + + /** + * @dev Получить URI логотипа токена (альтернативная функция для блокчейн-сканеров) + * @return URI логотипа или пустую строку если не установлен + */ + function logo() external view returns (string memory) { + return logoURI; + } + + /** + * @dev Получить информацию о мультичейн развертывании для блокчейн-сканеров + * @return chains Массив всех поддерживаемых chain ID (все сети равноправны) + * @return defaultVotingChain ID сети по умолчанию для голосования (может быть любая из поддерживаемых) + */ + function getMultichainInfo() external view returns (uint256[] memory chains, uint256 defaultVotingChain) { + return (supportedChainIds, block.chainid); + } + + /** + * @dev Получить адреса контракта в других сетях (для мультичейн сканеров) + * @return chainIds Массив chain ID где развернут контракт + * @return addresses Массив адресов контракта в соответствующих сетях + */ + function getMultichainAddresses() external view returns (uint256[] memory chainIds, address[] memory addresses) { + uint256[] memory chains = new uint256[](supportedChainIds.length); + address[] memory addrs = new address[](supportedChainIds.length); + + for (uint256 i = 0; i < supportedChainIds.length; i++) { + chains[i] = supportedChainIds[i]; + addrs[i] = address(this); // Детерминированный деплой обеспечивает одинаковые адреса + } + + return (chains, addrs); + } + + /** + * @dev Получить мультичейн метаданные в JSON формате для блокчейн-сканеров + * @return metadata JSON строка с информацией о мультичейн развертывании + * + * Архитектура: Single-Chain Governance - голосование происходит в одной сети, + * но исполнение может быть в любой из поддерживаемых сетей через подписи. + */ + function getMultichainMetadata() external view returns (string memory metadata) { + // Формируем JSON с информацией о мультичейн развертывании + string memory json = string(abi.encodePacked( + '{"multichain": {', + '"supportedChains": [' + )); + + for (uint256 i = 0; i < supportedChainIds.length; i++) { + if (i > 0) { + json = string(abi.encodePacked(json, ',')); + } + json = string(abi.encodePacked(json, _toString(supportedChainIds[i]))); + } + + json = string(abi.encodePacked( + json, + '],', + '"defaultVotingChain": ', + _toString(block.chainid), + ',', + '"note": "All chains are equal, voting can happen on any supported chain",', + '"contractAddress": "', + _toHexString(address(this)), + '"', + '}}' + )); + + return json; + } + + /** + * @dev Вспомогательная функция для конвертации uint256 в string + */ + function _toString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev Вспомогательная функция для конвертации address в hex string + */ + function _toHexString(address addr) internal pure returns (string memory) { + return _toHexString(abi.encodePacked(addr)); + } + + /** + * @dev Вспомогательная функция для конвертации bytes в hex string + */ + function _toHexString(bytes memory data) internal pure returns (string memory) { + bytes memory alphabet = "0123456789abcdef"; + bytes memory str = new bytes(2 + data.length * 2); + str[0] = "0"; + str[1] = "x"; + for (uint256 i = 0; i < data.length; i++) { + str[2 + i * 2] = alphabet[uint256(uint8(data[i] >> 4))]; + str[3 + i * 2] = alphabet[uint256(uint8(data[i] & 0x0f))]; + } + return string(str); + } + + + // API функции вынесены в отдельный reader контракт для экономии байт-кода + + // 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution + function getProposalState(uint256 _proposalId) public view returns (uint8 state) { + Proposal storage p = proposals[_proposalId]; + require(p.id == _proposalId, "Proposal does not exist"); + if (p.canceled) return 4; + if (p.executed) return 3; + (bool passed, bool quorumReached) = checkProposalResult(_proposalId); + bool votingOver = block.timestamp >= p.deadline; + bool ready = passed && quorumReached; + if (ready) return 5; // ReadyForExecution + if (passed && (votingOver || quorumReached)) return 1; // Succeeded + if (votingOver && !passed) return 2; // Defeated + return 0; // Pending + } + + // Функции для подсчёта голосов вынесены в reader контракт + + // Получить полную сводку по предложению + function getProposalSummary(uint256 _proposalId) external view returns ( + uint256 id, + string memory description, + uint256 forVotes, + uint256 againstVotes, + bool executed, + bool canceled, + uint256 deadline, + address initiator, + uint256 governanceChainId, + uint256 snapshotTimepoint, + uint256[] memory targetChains + ) { + Proposal storage p = proposals[_proposalId]; + require(p.id == _proposalId, "Proposal does not exist"); + + return ( + p.id, + p.description, + p.forVotes, + p.againstVotes, + p.executed, + p.canceled, + p.deadline, + p.initiator, + p.governanceChainId, + p.snapshotTimepoint, + p.targetChains + ); + } + + // Деактивация вынесена в отдельный модуль. См. DeactivationModule. + function isActive() external view returns (bool) { + return dleInfo.isActive; + } + // ===== Вспомогательные функции ===== + function _isTargetChain(Proposal storage p, uint256 chainId) internal view returns (bool) { + for (uint256 i = 0; i < p.targetChains.length; i++) { + if (p.targetChains[i] == chainId) return true; + } + return false; + } + + // ===== Overrides для ERC20Votes ===== + function _update(address from, address to, uint256 value) + internal + override(ERC20, ERC20Votes) + { + super._update(from, to, value); + } + + // Разрешение неоднозначности nonces между ERC20Permit и Nonces + function nonces(address owner) + public + view + override(ERC20Permit, Nonces) + returns (uint256) + { + return super.nonces(owner); + } + + // Запрет делегирования на третьих лиц: разрешено только делегировать самому себе + function _delegate(address delegator, address delegatee) internal override { + require(delegator == delegatee, "Delegation disabled"); + super._delegate(delegator, delegatee); + } + + // ===== Блокировка прямых переводов токенов ===== + // Токены DLE могут быть переведены только через governance + + /** + * @dev Блокирует прямые переводы токенов + * @return Всегда ревертится + */ + function transfer(address /*to*/, uint256 /*amount*/) public pure override returns (bool) { + // coverage:ignore-line + revert ErrTransfersDisabled(); + } + + /** + * @dev Блокирует прямые переводы токенов через approve/transferFrom + * @return Всегда ревертится + */ + function transferFrom(address /*from*/, address /*to*/, uint256 /*amount*/) public pure override returns (bool) { + // coverage:ignore-line + revert ErrTransfersDisabled(); + } + + /** + * @dev Блокирует прямые разрешения на перевод токенов + * @return Всегда ревертится + */ + function approve(address /*spender*/, uint256 /*amount*/) public pure override returns (bool) { + // coverage:ignore-line + revert ErrApprovalsDisabled(); + } +} diff --git a/backend/hardhat.config.js b/backend/hardhat.config.js index d9ace58..e391b92 100644 --- a/backend/hardhat.config.js +++ b/backend/hardhat.config.js @@ -19,12 +19,12 @@ function getNetworks() { // Базовая конфигурация сетей для верификации return { sepolia: { - url: process.env.SEPOLIA_RPC_URL || 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', + url: process.env.SEPOLIA_RPC_URL || 'https://1rpc.io/sepolia', chainId: 11155111, accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] }, holesky: { - url: process.env.HOLESKY_RPC_URL || 'https://ethereum-holesky-rpc.publicnode.com', + url: process.env.HOLESKY_RPC_URL || 'https://ethereum-holesky.publicnode.com', chainId: 17000, accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] }, @@ -52,9 +52,10 @@ module.exports = { settings: { optimizer: { enabled: true, - runs: 1 // Максимальная оптимизация размера для mainnet + runs: 0 // Максимальная оптимизация размера }, - viaIR: true + viaIR: true, + evmVersion: "paris" } }, contractSizer: { @@ -142,6 +143,9 @@ module.exports = { } ] }, + sourcify: { + enabled: true // Включаем Sourcify для децентрализованной верификации + }, solidityCoverage: { excludeContracts: [], skipFiles: [], diff --git a/backend/nodemon.json b/backend/nodemon.json index 0555e4b..552b084 100644 --- a/backend/nodemon.json +++ b/backend/nodemon.json @@ -8,8 +8,7 @@ "backend/artifacts/**", "backend/cache/**", "backend/contracts-data/**", - "backend/temp/**", - "backend/scripts/deploy/current-params*.json" + "backend/temp/**" ], "ext": "js,json" } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 6e11298..02bfc4d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,10 +22,10 @@ "run-migrations": "node scripts/run-migrations.js", "fix-duplicates": "node scripts/fix-duplicate-identities.js", "deploy:multichain": "node scripts/deploy/deploy-multichain.js", - "deploy:complete": "node scripts/deploy/deploy-dle-complete.js", "deploy:modules": "node scripts/deploy/deploy-modules.js", - "test:modules": "node scripts/test-modules-deploy.js", - "verify:contracts": "node scripts/verify-contracts.js" + "generate:abi": "node scripts/generate-abi.js", + "generate:flattened": "node scripts/generate-flattened.js", + "compile:full": "npx hardhat compile && npm run generate:abi && npm run generate:flattened" }, "dependencies": { "@anthropic-ai/sdk": "^0.51.0", @@ -80,7 +80,9 @@ "@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", + "@types/node": "^24.5.2", "chai": "^4.2.0", "eslint": "^9.21.0", "eslint-config-prettier": "^10.0.2", @@ -88,6 +90,7 @@ "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", "solidity-coverage": "^0.8.16", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 8acec30..0a0132c 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -43,6 +43,16 @@ router.get('/nonce', async (req, res) => { return res.status(400).json({ error: 'Address is required' }); } + // Очищаем истекшие nonce перед генерацией нового + try { + await db.getQuery()( + 'DELETE FROM nonces WHERE expires_at < NOW()' + ); + logger.info(`[nonce] Cleaned up expired nonces`); + } catch (cleanupError) { + logger.warn(`[nonce] Error cleaning up expired nonces: ${cleanupError.message}`); + } + // Генерируем случайный nonce const nonce = crypto.randomBytes(16).toString('hex'); logger.info(`[nonce] Generated nonce: ${nonce}`); @@ -136,9 +146,9 @@ router.post('/verify', async (req, res) => { console.error('Error reading encryption key:', keyError); } - // Проверяем nonce в базе данных + // Проверяем nonce в базе данных с проверкой времени истечения const nonceResult = await db.getQuery()( - 'SELECT nonce_encrypted FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)', + 'SELECT nonce_encrypted, expires_at FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)', [normalizedAddressLower, encryptionKey] ); @@ -147,6 +157,14 @@ router.post('/verify', async (req, res) => { return res.status(401).json({ success: false, error: 'Nonce not found' }); } + // Проверяем, не истек ли срок действия nonce + const expiresAt = new Date(nonceResult.rows[0].expires_at); + const now = new Date(); + if (now > expiresAt) { + logger.error(`[verify] Nonce expired for address: ${normalizedAddressLower}. Expired at: ${expiresAt}, Now: ${now}`); + return res.status(401).json({ success: false, error: 'Nonce expired' }); + } + // Расшифровываем nonce из базы данных const storedNonce = await db.getQuery()( 'SELECT decrypt_text(nonce_encrypted, $1) as nonce FROM nonces WHERE identity_value_encrypted = encrypt_text($2, $1)', @@ -156,9 +174,12 @@ router.post('/verify', async (req, res) => { logger.info(`[verify] Stored nonce from DB: ${storedNonce.rows[0]?.nonce}`); logger.info(`[verify] Nonce from request: ${nonce}`); logger.info(`[verify] Nonce match: ${storedNonce.rows[0]?.nonce === nonce}`); + logger.info(`[verify] Stored nonce length: ${storedNonce.rows[0]?.nonce?.length}`); + logger.info(`[verify] Request nonce length: ${nonce?.length}`); if (storedNonce.rows.length === 0 || storedNonce.rows[0].nonce !== nonce) { logger.error(`[verify] Invalid nonce for address: ${normalizedAddressLower}. Expected: ${storedNonce.rows[0]?.nonce}, Got: ${nonce}`); + logger.error(`[verify] Stored nonce type: ${typeof storedNonce.rows[0]?.nonce}, Request nonce type: ${typeof nonce}`); return res.status(401).json({ success: false, error: 'Invalid nonce' }); } diff --git a/backend/routes/blockchain.js b/backend/routes/blockchain.js index 28ef2b5..f74def0 100644 --- a/backend/routes/blockchain.js +++ b/backend/routes/blockchain.js @@ -15,6 +15,29 @@ const router = express.Router(); const { ethers } = require('ethers'); const rpcProviderService = require('../services/rpcProviderService'); +// Функция для получения списка сетей из БД для данного DLE +async function getSupportedChainIds(dleAddress) { + try { + const DeployParamsService = require('../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + const deployments = await deployParamsService.getAllDeployments(); + + // Находим деплой с данным адресом + for (const deployment of deployments) { + if (deployment.dleAddress === dleAddress && deployment.supportedChainIds) { + console.log(`[Blockchain] Найдены сети для DLE ${dleAddress}:`, deployment.supportedChainIds); + return deployment.supportedChainIds; + } + } + + // Fallback к стандартным сетям + return [17000, 11155111, 421614, 84532]; + } catch (error) { + console.error(`[Blockchain] Ошибка получения сетей для DLE ${dleAddress}:`, error); + return [17000, 11155111, 421614, 84532]; + } +} + // Чтение данных DLE из блокчейна router.post('/read-dle-info', async (req, res) => { try { @@ -31,7 +54,9 @@ router.post('/read-dle-info', async (req, res) => { // Определяем корректную сеть для данного адреса (или используем chainId из запроса) let provider, rpcUrl, targetChainId = req.body.chainId; - const candidateChainIds = [11155111, 17000, 421614, 84532]; + + // Получаем список сетей из базы данных для данного DLE + const candidateChainIds = await getSupportedChainIds(dleAddress); if (targetChainId) { rpcUrl = await rpcProviderService.getRpcUrlByChainId(Number(targetChainId)); if (!rpcUrl) { @@ -43,18 +68,46 @@ router.post('/read-dle-info', async (req, res) => { return res.status(400).json({ success: false, error: `По адресу ${dleAddress} нет контракта в сети ${targetChainId}` }); } } else { + // Ищем контракт во всех сетях + let foundContracts = []; + for (const cid of candidateChainIds) { try { const url = await rpcProviderService.getRpcUrlByChainId(cid); if (!url) continue; const prov = new ethers.JsonRpcProvider(url); const code = await prov.getCode(dleAddress); - if (code && code !== '0x') { provider = prov; rpcUrl = url; targetChainId = cid; break; } + if (code && code !== '0x') { + // Контракт найден, currentChainId теперь равен block.chainid + foundContracts.push({ + chainId: cid, + currentChainId: cid, // currentChainId = block.chainid = cid + provider: prov, + rpcUrl: url + }); + } } catch (_) {} } - if (!provider) { + + if (foundContracts.length === 0) { return res.status(400).json({ success: false, error: 'Не удалось найти сеть, где по адресу есть контракт' }); } + + // Выбираем первую доступную сеть (currentChainId - это governance chain, не primary) + const primaryContract = foundContracts[0]; + + if (primaryContract) { + // Используем основную сеть для чтения данных + provider = primaryContract.provider; + rpcUrl = primaryContract.rpcUrl; + targetChainId = primaryContract.chainId; + } else { + // Fallback: берем первый найденный контракт + const firstContract = foundContracts[0]; + provider = firstContract.provider; + rpcUrl = firstContract.rpcUrl; + targetChainId = firstContract.chainId; + } } // ABI для чтения данных DLE @@ -75,7 +128,7 @@ router.post('/read-dle-info', async (req, res) => { const dleInfo = await dle.getDLEInfo(); const totalSupply = await dle.totalSupply(); const quorumPercentage = await dle.quorumPercentage(); - const currentChainId = await dle.getCurrentChainId(); + const currentChainId = targetChainId; // currentChainId = block.chainid = targetChainId // Читаем логотип let logoURI = ''; @@ -205,6 +258,27 @@ router.post('/read-dle-info', async (req, res) => { console.log(`[Blockchain] Ошибка при чтении модулей:`, modulesError.message); } + // Собираем информацию о всех развернутых сетях + const deployedNetworks = []; + if (typeof foundContracts !== 'undefined') { + for (const contract of foundContracts) { + deployedNetworks.push({ + chainId: contract.chainId, + address: dleAddress, + currentChainId: contract.currentChainId, + isPrimary: false // currentChainId - это governance chain, не primary + }); + } + } else { + // Если chainId был указан в запросе, добавляем только эту сеть + deployedNetworks.push({ + chainId: targetChainId, + address: dleAddress, + currentChainId: Number(currentChainId), + isPrimary: Number(currentChainId) === targetChainId + }); + } + const blockchainData = { name: dleInfo.name, symbol: dleInfo.symbol, @@ -225,7 +299,8 @@ router.post('/read-dle-info', async (req, res) => { currentChainId: Number(currentChainId), rpcUsed: rpcUrl, participantCount: participantCount, - modules: modules // Информация о модулях + modules: modules, // Информация о модулях + deployedNetworks: deployedNetworks // Информация о всех развернутых сетях }; console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData); @@ -260,8 +335,30 @@ router.post('/get-proposals', async (req, res) => { console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -345,7 +442,7 @@ router.post('/get-proposals', async (req, res) => { initiator: proposal.initiator, governanceChainId: Number(proposal.governanceChainId), snapshotTimepoint: Number(proposal.snapshotTimepoint), - targetChains: proposal.targets.map(chainId => Number(chainId)), + targetChains: proposal.targets.map(targetChainId => Number(targetChainId)), isPassed: isPassed, blockNumber: events[i].blockNumber }; @@ -400,8 +497,30 @@ router.post('/get-proposal-info', async (req, res) => { console.log(`[Blockchain] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -424,7 +543,7 @@ router.post('/get-proposal-info', async (req, res) => { const isPassed = await dle.checkProposalResult(proposalId); // governanceChainId не сохраняется в предложении, используем текущую цепочку - const governanceChainId = 11155111; // Sepolia chain ID + const governanceChainId = targetChainId || 11155111; // Используем найденную сеть или Sepolia по умолчанию const proposalInfo = { description: proposal.description, @@ -472,8 +591,30 @@ router.post('/deactivate-dle', async (req, res) => { console.log(`[Blockchain] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -543,7 +684,30 @@ router.post('/check-deactivation-proposal-result', async (req, res) => { console.log(`[Blockchain] Проверка результата предложения деактивации: ${proposalId} для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -598,7 +762,30 @@ router.post('/load-deactivation-proposals', async (req, res) => { console.log(`[Blockchain] Загрузка предложений деактивации для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -679,7 +866,30 @@ router.post('/execute-proposal', async (req, res) => { console.log(`[Blockchain] Исполнение предложения ${proposalId} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -732,7 +942,30 @@ router.post('/cancel-proposal', async (req, res) => { console.log(`[Blockchain] Отмена предложения ${proposalId} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -794,7 +1027,30 @@ router.post('/get-governance-params', async (req, res) => { console.log(`[Blockchain] Получение параметров управления для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -847,7 +1103,30 @@ router.post('/get-proposal-state', async (req, res) => { console.log(`[Blockchain] Получение состояния предложения ${proposalId} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -899,7 +1178,30 @@ router.post('/get-proposal-votes', async (req, res) => { console.log(`[Blockchain] Получение голосов по предложению ${proposalId} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -954,7 +1256,30 @@ router.post('/get-proposals-count', async (req, res) => { console.log(`[Blockchain] Получение количества предложений для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1005,7 +1330,30 @@ router.post('/list-proposals', async (req, res) => { console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1058,7 +1406,30 @@ router.post('/get-voting-power-at', async (req, res) => { console.log(`[Blockchain] Получение голосующей силы для ${voter} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1111,7 +1482,30 @@ router.post('/get-quorum-at', async (req, res) => { console.log(`[Blockchain] Получение требуемого кворума для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1163,7 +1557,30 @@ router.post('/get-token-balance', async (req, res) => { console.log(`[Blockchain] Получение баланса токенов для ${account} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1215,7 +1632,30 @@ router.post('/get-total-supply', async (req, res) => { console.log(`[Blockchain] Получение общего предложения токенов для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1266,7 +1706,30 @@ router.post('/is-active', async (req, res) => { console.log(`[Blockchain] Проверка активности DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1323,7 +1786,30 @@ router.post('/get-dle-analytics', async (req, res) => { console.log(`[Blockchain] Получение аналитики DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1447,7 +1933,30 @@ router.post('/get-dle-history', async (req, res) => { console.log(`[Blockchain] Получение истории DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + const candidateChainIds = await getSupportedChainIds(dleAddress); + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, diff --git a/backend/routes/compile.js b/backend/routes/compile.js index d0ded71..564144d 100644 --- a/backend/routes/compile.js +++ b/backend/routes/compile.js @@ -47,6 +47,28 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res) => { hardhatProcess.on('close', (code) => { if (code === 0) { console.log('✅ Компиляция завершена успешно'); + + // Автоматически генерируем ABI для фронтенда + try { + const { generateABIFile } = require('../scripts/generate-abi'); + generateABIFile(); + console.log('✅ ABI файл автоматически обновлен'); + } catch (abiError) { + console.warn('⚠️ Ошибка генерации ABI:', abiError.message); + } + + // Автоматически генерируем flattened контракт для верификации + try { + const { generateFlattened } = require('../scripts/generate-flattened'); + generateFlattened().then(() => { + console.log('✅ Flattened контракт автоматически обновлен'); + }).catch((flattenError) => { + console.warn('⚠️ Ошибка генерации flattened контракта:', flattenError.message); + }); + } catch (flattenError) { + console.warn('⚠️ Ошибка генерации flattened контракта:', flattenError.message); + } + res.json({ success: true, message: 'Смарт-контракты скомпилированы успешно', diff --git a/backend/routes/dleAnalytics.js b/backend/routes/dleAnalytics.js index 5346571..c0db0b4 100644 --- a/backend/routes/dleAnalytics.js +++ b/backend/routes/dleAnalytics.js @@ -29,7 +29,41 @@ router.post('/get-dle-analytics', async (req, res) => { console.log(`[DLE Analytics] Получение аналитики для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -165,7 +199,41 @@ router.post('/get-dle-history', async (req, res) => { console.log(`[DLE Analytics] Получение истории для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, diff --git a/backend/routes/dleCore.js b/backend/routes/dleCore.js index 29243aa..7f96832 100644 --- a/backend/routes/dleCore.js +++ b/backend/routes/dleCore.js @@ -29,8 +29,41 @@ router.post('/read-dle-info', async (req, res) => { console.log(`[DLE Core] Чтение данных DLE из блокчейна: ${dleAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [11155111, 421614, 84532, 17000]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -205,11 +238,27 @@ router.post('/get-governance-params', async (req, res) => { console.log(`[DLE Core] Получение параметров управления для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Получаем RPC URL из параметров деплоя или используем Sepolia как fallback + let rpcUrl; + try { + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + const supportedChainIds = params.supportedChainIds || []; + const chainId = supportedChainIds.length > 0 ? supportedChainIds[0] : 11155111; + rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + } else { + rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем Sepolia:', error); + rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + } + if (!rpcUrl) { return res.status(500).json({ success: false, - error: 'RPC URL для Sepolia не найден' + error: 'RPC URL не найден' }); } @@ -258,11 +307,27 @@ router.post('/is-active', async (req, res) => { console.log(`[DLE Core] Проверка активности DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Получаем RPC URL из параметров деплоя или используем Sepolia как fallback + let rpcUrl; + try { + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + const supportedChainIds = params.supportedChainIds || []; + const chainId = supportedChainIds.length > 0 ? supportedChainIds[0] : 11155111; + rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + } else { + rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем Sepolia:', error); + rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + } + if (!rpcUrl) { return res.status(500).json({ success: false, - error: 'RPC URL для Sepolia не найден' + error: 'RPC URL не найден' }); } @@ -309,8 +374,41 @@ router.post('/deactivate-dle', async (req, res) => { console.log(`[DLE Core] Проверка возможности деактивации DLE: ${dleAddress} пользователем: ${userAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [11155111, 421614, 84532, 17000]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, diff --git a/backend/routes/dleHistory.js b/backend/routes/dleHistory.js index dedfbd8..ac18350 100644 --- a/backend/routes/dleHistory.js +++ b/backend/routes/dleHistory.js @@ -30,7 +30,41 @@ router.post('/get-extended-history', async (req, res) => { console.log(`[DLE History] Получение расширенной истории для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, diff --git a/backend/routes/dleModules.js b/backend/routes/dleModules.js index eb12b85..54b66d7 100644 --- a/backend/routes/dleModules.js +++ b/backend/routes/dleModules.js @@ -575,6 +575,26 @@ router.post('/get-all-modules', async (req, res) => { return networks[chainId] || `Chain ${chainId}`; } + function getFallbackRpcUrl(chainId) { + const fallbackUrls = { + 11155111: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', + 17000: 'https://ethereum-holesky.publicnode.com', + 421614: 'https://sepolia-rollup.arbitrum.io/rpc', + 84532: 'https://sepolia.base.org' + }; + return fallbackUrls[chainId] || null; + } + + function getEtherscanUrl(chainId) { + const etherscanUrls = { + 11155111: 'https://sepolia.etherscan.io', + 17000: 'https://holesky.etherscan.io', + 421614: 'https://sepolia.arbiscan.io', + 84532: 'https://sepolia.basescan.org' + }; + return etherscanUrls[chainId] || null; + } + function getModuleDescription(moduleType) { const descriptions = { treasury: 'Казначейство DLE - управление финансами, депозиты, выводы, дивиденды', @@ -590,37 +610,57 @@ router.post('/get-all-modules', async (req, res) => { console.log(`[DLE Modules] Найдено типов модулей: ${formattedModules.length}`); - // Получаем поддерживаемые сети из модулей - const supportedNetworks = [ - { - chainId: 11155111, - networkName: 'Sepolia', - rpcUrl: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', - etherscanUrl: 'https://sepolia.etherscan.io', - networkIndex: 0 - }, - { - chainId: 17000, - networkName: 'Holesky', - rpcUrl: 'https://ethereum-holesky.publicnode.com', - etherscanUrl: 'https://holesky.etherscan.io', - networkIndex: 1 - }, - { - chainId: 421614, - networkName: 'Arbitrum Sepolia', - rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc', - etherscanUrl: 'https://sepolia.arbiscan.io', - networkIndex: 2 - }, - { - chainId: 84532, - networkName: 'Base Sepolia', - rpcUrl: 'https://sepolia.base.org', - etherscanUrl: 'https://sepolia.basescan.org', - networkIndex: 3 + // Получаем поддерживаемые сети из параметров деплоя + let supportedNetworks = []; + try { + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + const supportedChainIds = params.supportedChainIds || []; + const rpcUrls = params.rpcUrls || params.rpc_urls || {}; + + supportedNetworks = supportedChainIds.map((chainId, index) => ({ + chainId: Number(chainId), + networkName: getNetworkName(Number(chainId)), + rpcUrl: rpcUrls[chainId] || getFallbackRpcUrl(chainId), + etherscanUrl: getEtherscanUrl(chainId), + networkIndex: index + })); } - ]; + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя:', error); + // Fallback для совместимости + supportedNetworks = [ + { + chainId: 11155111, + networkName: 'Sepolia', + rpcUrl: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', + etherscanUrl: 'https://sepolia.etherscan.io', + networkIndex: 0 + }, + { + chainId: 17000, + networkName: 'Holesky', + rpcUrl: 'https://ethereum-holesky.publicnode.com', + etherscanUrl: 'https://holesky.etherscan.io', + networkIndex: 1 + }, + { + chainId: 421614, + networkName: 'Arbitrum Sepolia', + rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc', + etherscanUrl: 'https://sepolia.arbiscan.io', + networkIndex: 2 + }, + { + chainId: 84532, + networkName: 'Base Sepolia', + rpcUrl: 'https://sepolia.base.org', + etherscanUrl: 'https://sepolia.basescan.org', + networkIndex: 3 + } + ]; + } res.json({ success: true, @@ -642,10 +682,57 @@ router.post('/get-all-modules', async (req, res) => { } }); -// Создать предложение о добавлении модуля +// Получить deploymentId по адресу DLE +router.post('/get-deployment-id', async (req, res) => { + try { + const { dleAddress } = req.body; + + if (!dleAddress) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE обязателен' + }); + } + + console.log(`[DLE Modules] Поиск deploymentId для DLE: ${dleAddress}`); + + const DeployParamsService = require('../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + + // Ищем параметры деплоя по адресу DLE + const result = await deployParamsService.getDeployParamsByDleAddress(dleAddress); + + if (!result) { + return res.status(404).json({ + success: false, + error: 'DeploymentId не найден для данного адреса DLE' + }); + } + + await deployParamsService.close(); + + res.json({ + success: true, + data: { + deploymentId: result.deployment_id, + dleAddress: result.dle_address, + deploymentStatus: result.deployment_status + } + }); + + } catch (error) { + console.error('[DLE Modules] Ошибка при получении deploymentId:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при получении deploymentId: ' + error.message + }); + } +}); + +// Создать предложение о добавлении модуля (с автоматической оплатой газа) router.post('/create-add-module-proposal', async (req, res) => { try { - const { dleAddress, description, duration, moduleId, moduleAddress, chainId } = req.body; + const { dleAddress, description, duration, moduleId, moduleAddress, chainId, deploymentId } = req.body; if (!dleAddress || !description || !duration || !moduleId || !moduleAddress || !chainId) { return res.status(400).json({ @@ -666,14 +753,54 @@ router.post('/create-add-module-proposal', async (req, res) => { const provider = new ethers.JsonRpcProvider(rpcUrl); + // Получаем приватный ключ из параметров деплоя + let privateKey; + if (deploymentId) { + const DeployParamsService = require('../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + const params = await deployParamsService.getDeployParams(deploymentId); + + if (!params || !params.privateKey) { + return res.status(400).json({ + success: false, + error: 'Приватный ключ не найден в параметрах деплоя' + }); + } + + privateKey = params.privateKey; + await deployParamsService.close(); + } else { + // Fallback к переменной окружения + privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + return res.status(400).json({ + success: false, + error: 'Приватный ключ не найден. Укажите deploymentId или установите PRIVATE_KEY' + }); + } + } + + // Создаем кошелек + const wallet = new ethers.Wallet(privateKey, provider); + console.log(`[DLE Modules] Используем кошелек: ${wallet.address}`); + const dleAbi = [ "function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)" ]; - const dle = new ethers.Contract(dleAddress, dleAbi, provider); + const dle = new ethers.Contract(dleAddress, dleAbi, wallet); - // Подготавливаем данные для транзакции (не отправляем) - const txData = await dle.createAddModuleProposal.populateTransaction( + // Отправляем транзакцию автоматически + console.log(`[DLE Modules] Отправляем транзакцию создания предложения...`); + console.log(`[DLE Modules] Параметры:`, { + description, + duration, + moduleId, + moduleAddress, + chainId + }); + + const tx = await dle.createAddModuleProposal( description, duration, moduleId, @@ -681,16 +808,130 @@ router.post('/create-add-module-proposal', async (req, res) => { chainId ); - console.log(`[DLE Modules] Данные транзакции подготовлены:`, txData); + console.log(`[DLE Modules] Транзакция отправлена: ${tx.hash}`); + console.log(`[DLE Modules] Ожидаем подтверждения...`); + + // Ждем подтверждения + const receipt = await tx.wait(); + + // Пробуем получить proposalId из возвращаемого значения транзакции + let proposalIdFromReturn = null; + try { + // Если функция возвращает значение, оно должно быть в receipt + if (receipt.logs && receipt.logs.length > 0) { + console.log(`[DLE Modules] Ищем ProposalCreated в ${receipt.logs.length} логах транзакции...`); + + // Ищем событие с возвращаемым значением + for (let i = 0; i < receipt.logs.length; i++) { + const log = receipt.logs[i]; + console.log(`[DLE Modules] Лог ${i}:`, { + address: log.address, + topics: log.topics, + data: log.data + }); + + try { + const parsedLog = dle.interface.parseLog(log); + console.log(`[DLE Modules] Парсинг лога ${i}:`, parsedLog); + + if (parsedLog && parsedLog.name === 'ProposalCreated') { + proposalIdFromReturn = parsedLog.args.proposalId.toString(); + console.log(`[DLE Modules] ✅ Получен proposalId из события: ${proposalIdFromReturn}`); + break; + } + } catch (e) { + console.log(`[DLE Modules] Ошибка парсинга лога ${i}:`, e.message); + // Пробуем альтернативный способ - ищем по топикам + if (log.topics && log.topics.length > 0) { + // ProposalCreated имеет сигнатуру: ProposalCreated(uint256,address,string) + // Первый топик - это хеш сигнатуры события + const proposalCreatedTopic = ethers.id("ProposalCreated(uint256,address,string)"); + if (log.topics[0] === proposalCreatedTopic) { + console.log(`[DLE Modules] Найден топик ProposalCreated, извлекаем proposalId из данных...`); + // proposalId находится в indexed параметрах (топиках) + if (log.topics.length > 1) { + proposalIdFromReturn = BigInt(log.topics[1]).toString(); + console.log(`[DLE Modules] ✅ Извлечен proposalId из топика: ${proposalIdFromReturn}`); + break; + } + } + } + } + } + } + } catch (e) { + console.log(`[DLE Modules] Ошибка при получении proposalId из возвращаемого значения:`, e.message); + } + console.log(`[DLE Modules] Транзакция подтверждена:`, { + hash: receipt.hash, + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed.toString(), + logsCount: receipt.logs.length, + status: receipt.status + }); + + // Используем proposalId из события, если он найден + let proposalId = proposalIdFromReturn; + + // Если не найден в событии, пробуем другие способы + if (!proposalId) { + console.log(`[DLE Modules] Анализируем ${receipt.logs.length} логов для поиска ProposalCreated...`); + + if (receipt.logs && receipt.logs.length > 0) { + // Ищем событие ProposalCreated + for (let i = 0; i < receipt.logs.length; i++) { + const log = receipt.logs[i]; + console.log(`[DLE Modules] Лог ${i}:`, { + address: log.address, + topics: log.topics, + data: log.data + }); + + try { + const parsedLog = dle.interface.parseLog(log); + console.log(`[DLE Modules] Парсинг лога ${i}:`, parsedLog); + + if (parsedLog && parsedLog.name === 'ProposalCreated') { + proposalId = parsedLog.args.proposalId.toString(); + console.log(`[DLE Modules] ✅ Найден ProposalCreated с ID: ${proposalId}`); + break; + } + } catch (e) { + console.log(`[DLE Modules] Ошибка парсинга лога ${i}:`, e.message); + // Пробуем альтернативный способ - ищем по топикам + if (log.topics && log.topics.length > 0) { + // ProposalCreated имеет сигнатуру: ProposalCreated(uint256,address,string) + // Первый топик - это хеш сигнатуры события + const proposalCreatedTopic = ethers.id("ProposalCreated(uint256,address,string)"); + if (log.topics[0] === proposalCreatedTopic) { + console.log(`[DLE Modules] Найден топик ProposalCreated, извлекаем proposalId из данных...`); + // proposalId находится в indexed параметрах (топиках) + if (log.topics.length > 1) { + proposalId = BigInt(log.topics[1]).toString(); + console.log(`[DLE Modules] ✅ Извлечен proposalId из топика: ${proposalId}`); + break; + } + } + } + } + } + } + } + + if (!proposalId) { + console.warn(`[DLE Modules] ⚠️ Не удалось извлечь proposalId из логов транзакции`); + console.warn(`[DLE Modules] ⚠️ Это критическая проблема - без proposalId нельзя исполнить предложение!`); + } else { + console.log(`[DLE Modules] ✅ Успешно получен proposalId: ${proposalId}`); + } res.json({ success: true, data: { - to: dleAddress, - data: txData.data, - value: "0x0", - gasLimit: "0x1e8480", // 2,000,000 gas - message: "Подготовлены данные для создания предложения о добавлении модуля. Отправьте транзакцию через MetaMask." + transactionHash: receipt.hash, + proposalId: proposalId, + gasUsed: receipt.gasUsed.toString(), + message: `Предложение о добавлении модуля успешно создано! ID: ${proposalId || 'неизвестно'}` } }); @@ -717,7 +958,41 @@ router.post('/create-remove-module-proposal', async (req, res) => { console.log(`[DLE Modules] Создание предложения об удалении модуля: ${moduleId} для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -1255,8 +1530,9 @@ async function createStandardJsonInput(contractName, moduleAddress, dleAddress, settings: { optimizer: { enabled: true, - runs: 200 + runs: 0 }, + viaIR: true, evmVersion: "paris", outputSelection: { "*": { @@ -1265,7 +1541,7 @@ async function createStandardJsonInput(contractName, moduleAddress, dleAddress, }, libraries: {}, remappings: [ - "@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/" + "@openzeppelin/contracts/=@openzeppelin/contracts/" ] } }; @@ -1904,8 +2180,9 @@ router.post('/deploy-module-all-networks', async (req, res) => { const provider = new ethers.JsonRpcProvider(network.rpcUrl); const wallet = new ethers.Wallet(privateKey, provider); - // Получаем текущий nonce - const currentNonce = await wallet.getNonce(); + // Используем NonceManager для правильного управления nonce + const { nonceManager } = require('../utils/nonceManager'); + const currentNonce = await nonceManager.getNonce(wallet.address, network.rpcUrl, network.chainId); console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`); // Получаем фабрику контракта @@ -2057,7 +2334,7 @@ router.post('/verify-dle-all-networks', async (req, res) => { const supportedChainIds = Array.isArray(saved?.networks) ? saved.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v)) : (saved?.governanceSettings?.supportedChainIds || []); - const currentChainId = Number(saved?.governanceSettings?.currentChainId || network.chainId); + const currentChainId = Number(saved?.governanceSettings?.currentChainId || 1); // governance chain, не network.chainId // Создаем стандартный JSON input для верификации const standardJsonInput = { @@ -2070,7 +2347,7 @@ router.post('/verify-dle-all-networks', async (req, res) => { settings: { optimizer: { enabled: true, - runs: 1 + runs: 0 }, viaIR: true, outputSelection: { @@ -2123,7 +2400,7 @@ router.post('/verify-dle-all-networks', async (req, res) => { const initPartners = Array.isArray(found?.initialPartners) ? found.initialPartners : []; const initAmounts = Array.isArray(found?.initialAmounts) ? found.initialAmounts : []; const scIds = Array.isArray(found?.networks) ? found.networks.map(n => Number(n.chainId)).filter(v => !isNaN(v)) : supportedChainIds; - const currentCid = Number(found?.governanceSettings?.currentChainId || found?.networks?.[0]?.chainId || network.chainId); + const currentCid = Number(found?.governanceSettings?.currentChainId || 1); // governance chain, не network.chainId const encoded = ethers.AbiCoder.defaultAbiCoder().encode( ['tuple(string,string,string,string,uint256,string,uint256,uint256,address[],uint256[],uint256[])', 'uint256', 'address'], [[name, symbol, location, coordinates, jurisdiction, oktmo, kpp, quorumPercentage, initPartners, initAmounts.map(a => BigInt(a)), scIds], BigInt(currentCid), initializer] diff --git a/backend/routes/dleMultichain.js b/backend/routes/dleMultichain.js index 44766a5..d85b1af 100644 --- a/backend/routes/dleMultichain.js +++ b/backend/routes/dleMultichain.js @@ -1,442 +1,124 @@ -/** - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - * All rights reserved. - * - * This software is proprietary and confidential. - * Unauthorized copying, modification, or distribution is prohibited. - * - * For licensing inquiries: info@hb3-accelerator.com - * Website: https://hb3-accelerator.com - * GitHub: https://github.com/HB3-ACCELERATOR - */ - const express = require('express'); const router = express.Router(); -const { ethers } = require('ethers'); -const rpcProviderService = require('../services/rpcProviderService'); +const deployParamsService = require('../services/deployParamsService'); -// Получить поддерживаемые сети -router.post('/get-supported-chains', async (req, res) => { +/** + * Получить адрес контракта в указанной сети для мультичейн голосования + * POST /api/dle-core/get-multichain-contracts + */ +router.post('/get-multichain-contracts', async (req, res) => { try { - const { dleAddress } = req.body; + const { originalContract, targetChainId } = req.body; - if (!dleAddress) { + console.log('🔍 [MULTICHAIN] Поиск контракта для мультичейн голосования:', { + originalContract, + targetChainId + }); + + if (!originalContract || !targetChainId) { return res.status(400).json({ success: false, - error: 'Адрес DLE обязателен' + error: 'Не указан originalContract или targetChainId' }); } - - console.log(`[DLE Multichain] Получение поддерживаемых сетей для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { + + // Ищем контракт в указанной сети + // Для мультичейн контрактов с одинаковым адресом (детерминированный деплой) + // или контракты в разных сетях с разными адресами + + // Сначала проверяем, есть ли контракт с таким же адресом в целевой сети + const contractsInTargetNetwork = await deployParamsService.getContractsByChainId(targetChainId); + + console.log('📊 [MULTICHAIN] Контракты в целевой сети:', contractsInTargetNetwork); + + // Ищем контракт в целевой сети (все контракты в targetChainId уже отфильтрованы) + const targetContract = contractsInTargetNetwork[0]; // Берем первый контракт в целевой сети + + if (targetContract) { + console.log('✅ [MULTICHAIN] Найден контракт в целевой сети:', targetContract.dleAddress); + + return res.json({ + success: true, + contractAddress: targetContract.dleAddress, + chainId: targetChainId, + source: 'database' + }); + } + + // Если не найден контракт в целевой сети, проверяем мультичейн развертывание + // с одинаковым адресом (CREATE2) + const { ethers } = require('ethers'); + + // Получаем RPC URL из параметров деплоя + let rpcUrl; + try { + // Получаем последние параметры деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + const rpcUrls = params.rpcUrls || params.rpc_urls || {}; + rpcUrl = rpcUrls[targetChainId]; + } + + // Если не найден в параметрах, используем fallback + if (!rpcUrl) { + const fallbackConfigs = { + '11155111': 'https://1rpc.io/sepolia', + '17000': 'https://ethereum-holesky.publicnode.com', + '421614': 'https://sepolia-rollup.arbitrum.io/rpc', + '84532': 'https://sepolia.base.org' + }; + rpcUrl = fallbackConfigs[targetChainId]; + } + + if (!rpcUrl) { + return res.status(400).json({ + success: false, + error: `Неподдерживаемая сеть: ${targetChainId}` + }); + } + } catch (error) { + console.error('❌ Ошибка получения RPC URL:', error); return res.status(500).json({ success: false, - error: 'RPC URL для Sepolia не найден' + error: 'Ошибка получения конфигурации сети' }); } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - const dleAbi = [ - "function getSupportedChainCount() external view returns (uint256)", - "function getSupportedChainId(uint256 _index) external view returns (uint256)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Получаем количество поддерживаемых сетей - const chainCount = await dle.getSupportedChainCount(); - - // Получаем ID каждой сети - const supportedChains = []; - for (let i = 0; i < Number(chainCount); i++) { - const chainId = await dle.getSupportedChainId(i); - supportedChains.push(chainId); - } - - console.log(`[DLE Multichain] Поддерживаемые сети:`, supportedChains); - - res.json({ - success: true, - data: { - chains: supportedChains.map(chainId => Number(chainId)) + try { + const provider = new ethers.JsonRpcProvider(rpcUrl); + const contractCode = await provider.getCode(originalContract); + + if (contractCode && contractCode !== '0x') { + console.log('✅ [MULTICHAIN] Контракт существует в целевой сети с тем же адресом (CREATE2)'); + + return res.json({ + success: true, + contractAddress: originalContract, + chainId: targetChainId, + source: 'blockchain' + }); } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при получении поддерживаемых сетей:', error); - res.status(500).json({ + } catch (blockchainError) { + console.warn('⚠️ [MULTICHAIN] Ошибка проверки контракта в блокчейне:', blockchainError.message); + } + + // Контракт не найден + console.log('❌ [MULTICHAIN] Контракт не найден в целевой сети'); + + return res.json({ success: false, - error: 'Ошибка при получении поддерживаемых сетей: ' + error.message + error: 'Контракт не найден в целевой сети' + }); + + } catch (error) { + console.error('❌ [MULTICHAIN] Ошибка поиска мультичейн контракта:', error); + + return res.status(500).json({ + success: false, + error: 'Внутренняя ошибка сервера' }); } }); -// Проверить поддержку сети -router.post('/is-chain-supported', async (req, res) => { - try { - const { dleAddress, chainId } = req.body; - - if (!dleAddress || chainId === undefined) { - return res.status(400).json({ - success: false, - error: 'Адрес DLE и ID сети обязательны' - }); - } - - console.log(`[DLE Multichain] Проверка поддержки сети ${chainId} для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - - const dleAbi = [ - "function isChainSupported(uint256 _chainId) external view returns (bool)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Проверяем поддержку сети - const isSupported = await dle.isChainSupported(chainId); - - console.log(`[DLE Multichain] Поддержка сети ${chainId}: ${isSupported}`); - - res.json({ - success: true, - data: { - chainId: Number(chainId), - isSupported: isSupported - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при проверке поддержки сети:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при проверке поддержки сети: ' + error.message - }); - } -}); - -// Получить количество поддерживаемых сетей -router.post('/get-supported-chain-count', async (req, res) => { - try { - const { dleAddress } = req.body; - - if (!dleAddress) { - return res.status(400).json({ - success: false, - error: 'Адрес DLE обязателен' - }); - } - - console.log(`[DLE Multichain] Получение количества поддерживаемых сетей для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - - const dleAbi = [ - "function getSupportedChainCount() external view returns (uint256)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Получаем количество поддерживаемых сетей - const count = await dle.getSupportedChainCount(); - - console.log(`[DLE Multichain] Количество поддерживаемых сетей: ${count}`); - - res.json({ - success: true, - data: { - count: Number(count) - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при получении количества поддерживаемых сетей:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при получении количества поддерживаемых сетей: ' + error.message - }); - } -}); - -// Получить ID сети по индексу -router.post('/get-supported-chain-id', async (req, res) => { - try { - const { dleAddress, index } = req.body; - - if (!dleAddress || index === undefined) { - return res.status(400).json({ - success: false, - error: 'Адрес DLE и индекс обязательны' - }); - } - - console.log(`[DLE Multichain] Получение ID сети по индексу ${index} для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - - const dleAbi = [ - "function getSupportedChainId(uint256 _index) external view returns (uint256)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Получаем ID сети по индексу - const chainId = await dle.getSupportedChainId(index); - - console.log(`[DLE Multichain] ID сети по индексу ${index}: ${chainId}`); - - res.json({ - success: true, - data: { - index: Number(index), - chainId: Number(chainId) - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при получении ID сети по индексу:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при получении ID сети по индексу: ' + error.message - }); - } -}); - -// Проверить подключение к сети -router.post('/check-chain-connection', async (req, res) => { - try { - const { dleAddress, chainId } = req.body; - - if (!dleAddress || chainId === undefined) { - return res.status(400).json({ - success: false, - error: 'Адрес DLE и ID сети обязательны' - }); - } - - console.log(`[DLE Multichain] Проверка подключения к сети ${chainId} для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - - const dleAbi = [ - "function checkChainConnection(uint256 _chainId) external view returns (bool)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Проверяем подключение к сети - const isAvailable = await dle.checkChainConnection(chainId); - - console.log(`[DLE Multichain] Подключение к сети ${chainId}: ${isAvailable}`); - - res.json({ - success: true, - data: { - chainId: Number(chainId), - isAvailable: isAvailable - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при проверке подключения к сети:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при проверке подключения к сети: ' + error.message - }); - } -}); - -// Проверить готовность к синхронизации -router.post('/check-sync-readiness', async (req, res) => { - try { - const { dleAddress, proposalId } = req.body; - - if (!dleAddress || proposalId === undefined) { - return res.status(400).json({ - success: false, - error: 'Адрес DLE и ID предложения обязательны' - }); - } - - console.log(`[DLE Multichain] Проверка готовности к синхронизации предложения ${proposalId} для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - - const dleAbi = [ - "function checkSyncReadiness(uint256 _proposalId) external view returns (bool)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Проверяем готовность к синхронизации - const allChainsReady = await dle.checkSyncReadiness(proposalId); - - console.log(`[DLE Multichain] Готовность к синхронизации предложения ${proposalId}: ${allChainsReady}`); - - res.json({ - success: true, - data: { - proposalId: Number(proposalId), - allChainsReady: allChainsReady - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при проверке готовности к синхронизации:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при проверке готовности к синхронизации: ' + error.message - }); - } -}); - -// Синхронизировать во все сети -router.post('/sync-to-all-chains', async (req, res) => { - try { - const { dleAddress, proposalId, userAddress, privateKey } = req.body; - - if (!dleAddress || proposalId === undefined || !userAddress || !privateKey) { - return res.status(400).json({ - success: false, - error: 'Все поля обязательны, включая приватный ключ' - }); - } - - console.log(`[DLE Multichain] Синхронизация предложения ${proposalId} во все сети для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(privateKey, provider); - - const dleAbi = [ - "function syncToAllChains(uint256 _proposalId) external" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, wallet); - - // Синхронизируем во все сети - const tx = await dle.syncToAllChains(proposalId); - const receipt = await tx.wait(); - - console.log(`[DLE Multichain] Синхронизация выполнена:`, receipt); - - res.json({ - success: true, - data: { - transactionHash: receipt.hash - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при синхронизации во все сети:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при синхронизации во все сети: ' + error.message - }); - } -}); - -// Исполнить предложение по подписям -router.post('/execute-proposal-by-signatures', async (req, res) => { - try { - const { dleAddress, proposalId, signatures, userAddress, privateKey } = req.body; - - if (!dleAddress || proposalId === undefined || !signatures || !userAddress || !privateKey) { - return res.status(400).json({ - success: false, - error: 'Все поля обязательны, включая приватный ключ' - }); - } - - console.log(`[DLE Multichain] Исполнение предложения ${proposalId} по подписям для DLE: ${dleAddress}`); - - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(privateKey, provider); - - const dleAbi = [ - "function executeProposalBySignatures(uint256 _proposalId, bytes[] calldata _signatures) external" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, wallet); - - // Исполняем предложение по подписям - const tx = await dle.executeProposalBySignatures(proposalId, signatures); - const receipt = await tx.wait(); - - console.log(`[DLE Multichain] Предложение исполнено по подписям:`, receipt); - - res.json({ - success: true, - data: { - transactionHash: receipt.hash - } - }); - - } catch (error) { - console.error('[DLE Multichain] Ошибка при исполнении предложения по подписям:', error); - res.status(500).json({ - success: false, - error: 'Ошибка при исполнении предложения по подписям: ' + error.message - }); - } -}); - -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/routes/dleMultichainExecution.js b/backend/routes/dleMultichainExecution.js new file mode 100644 index 0000000..beef8b5 --- /dev/null +++ b/backend/routes/dleMultichainExecution.js @@ -0,0 +1,346 @@ +/** + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + * All rights reserved. + * + * This software is proprietary and confidential. + * Unauthorized copying, modification, or distribution is prohibited. + * + * For licensing inquiries: info@hb3-accelerator.com + * Website: https://hb3-accelerator.com + * GitHub: https://github.com/HB3-ACCELERATOR + */ + +const express = require('express'); +const router = express.Router(); +const { ethers } = require('ethers'); +const rpcProviderService = require('../services/rpcProviderService'); +const DeployParamsService = require('../services/deployParamsService'); + +/** + * Получить информацию о мультиконтрактном предложении + * @route POST /api/dle-multichain/get-proposal-multichain-info + */ +router.post('/get-proposal-multichain-info', async (req, res) => { + try { + const { dleAddress, proposalId, governanceChainId } = req.body; + + if (!dleAddress || proposalId === undefined || !governanceChainId) { + return res.status(400).json({ + success: false, + error: 'Адрес DLE, ID предложения и ID сети голосования обязательны' + }); + } + + console.log(`[DLE Multichain] Получение информации о предложении ${proposalId} для DLE: ${dleAddress}`); + + // Получаем RPC URL для сети голосования + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(governanceChainId); + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: `RPC URL для сети ${governanceChainId} не найден` + }); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + const dleAbi = [ + "function proposals(uint256) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, bytes memory operation, uint256 governanceChainId, uint256 snapshotTimepoint, uint256[] memory targetChains)", + "function getProposalState(uint256 _proposalId) external view returns (uint8 state)", + "function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)", + "function getSupportedChainCount() external view returns (uint256)", + "function getSupportedChainId(uint256 _index) external view returns (uint256)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Получаем данные предложения + const proposal = await dle.proposals(proposalId); + const state = await dle.getProposalState(proposalId); + const result = await dle.checkProposalResult(proposalId); + + // Получаем поддерживаемые сети + const chainCount = await dle.getSupportedChainCount(); + const supportedChains = []; + for (let i = 0; i < chainCount; i++) { + const chainId = await dle.getSupportedChainId(i); + supportedChains.push(Number(chainId)); + } + + const proposalInfo = { + id: Number(proposal.id), + description: proposal.description, + forVotes: Number(proposal.forVotes), + againstVotes: Number(proposal.againstVotes), + executed: proposal.executed, + canceled: proposal.canceled, + deadline: Number(proposal.deadline), + initiator: proposal.initiator, + operation: proposal.operation, + governanceChainId: Number(proposal.governanceChainId), + targetChains: proposal.targetChains.map(chain => Number(chain)), + snapshotTimepoint: Number(proposal.snapshotTimepoint), + state: Number(state), + isPassed: result.passed, + quorumReached: result.quorumReached, + supportedChains: supportedChains, + canExecuteInTargetChains: result.passed && result.quorumReached && !proposal.executed && !proposal.canceled + }; + + console.log(`[DLE Multichain] Информация о предложении получена:`, proposalInfo); + + res.json({ + success: true, + data: proposalInfo + }); + + } catch (error) { + console.error('[DLE Multichain] Ошибка при получении информации о предложении:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при получении информации о предложении: ' + error.message + }); + } +}); + +/** + * Исполнить предложение во всех целевых сетях + * @route POST /api/dle-multichain/execute-in-all-target-chains + */ +router.post('/execute-in-all-target-chains', async (req, res) => { + try { + const { dleAddress, proposalId, deploymentId, userAddress } = req.body; + + if (!dleAddress || proposalId === undefined || !deploymentId || !userAddress) { + return res.status(400).json({ + success: false, + error: 'Все поля обязательны' + }); + } + + console.log(`[DLE Multichain] Исполнение предложения ${proposalId} во всех целевых сетях для DLE: ${dleAddress}`); + + // Получаем параметры деплоя + const deployParamsService = new DeployParamsService(); + const deployParams = await deployParamsService.getDeployParams(deploymentId); + + if (!deployParams || !deployParams.privateKey) { + return res.status(400).json({ + success: false, + error: 'Приватный ключ не найден в параметрах деплоя' + }); + } + + // Получаем информацию о предложении + const proposalInfoResponse = await fetch(`${req.protocol}://${req.get('host')}/api/dle-multichain/get-proposal-multichain-info`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + dleAddress, + proposalId, + governanceChainId: deployParams.currentChainId + }) + }); + + const proposalInfo = await proposalInfoResponse.json(); + + if (!proposalInfo.success) { + return res.status(400).json({ + success: false, + error: 'Не удалось получить информацию о предложении' + }); + } + + const { targetChains, canExecuteInTargetChains } = proposalInfo.data; + + if (!canExecuteInTargetChains) { + return res.status(400).json({ + success: false, + error: 'Предложение не готово к исполнению в целевых сетях' + }); + } + + if (targetChains.length === 0) { + return res.status(400).json({ + success: false, + error: 'У предложения нет целевых сетей для исполнения' + }); + } + + // Исполняем в каждой целевой сети + const executionResults = []; + + for (const targetChainId of targetChains) { + try { + console.log(`[DLE Multichain] Исполнение в сети ${targetChainId}`); + + const result = await executeProposalInChain( + dleAddress, + proposalId, + targetChainId, + deployParams.privateKey, + userAddress + ); + + executionResults.push({ + chainId: targetChainId, + success: true, + transactionHash: result.transactionHash + }); + + } catch (error) { + console.error(`[DLE Multichain] Ошибка исполнения в сети ${targetChainId}:`, error.message); + executionResults.push({ + chainId: targetChainId, + success: false, + error: error.message + }); + } + } + + const successCount = executionResults.filter(r => r.success).length; + const totalCount = executionResults.length; + + console.log(`[DLE Multichain] Исполнение завершено: ${successCount}/${totalCount} успешно`); + + res.json({ + success: true, + data: { + proposalId, + targetChains, + executionResults, + summary: { + total: totalCount, + successful: successCount, + failed: totalCount - successCount + } + } + }); + + } catch (error) { + console.error('[DLE Multichain] Ошибка при исполнении во всех целевых сетях:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при исполнении во всех целевых сетях: ' + error.message + }); + } +}); + +/** + * Исполнить предложение в конкретной целевой сети + * @route POST /api/dle-multichain/execute-in-target-chain + */ +router.post('/execute-in-target-chain', async (req, res) => { + try { + const { dleAddress, proposalId, targetChainId, deploymentId, userAddress } = req.body; + + if (!dleAddress || proposalId === undefined || !targetChainId || !deploymentId || !userAddress) { + return res.status(400).json({ + success: false, + error: 'Все поля обязательны' + }); + } + + console.log(`[DLE Multichain] Исполнение предложения ${proposalId} в сети ${targetChainId} для DLE: ${dleAddress}`); + + // Получаем параметры деплоя + const deployParamsService = new DeployParamsService(); + const deployParams = await deployParamsService.getDeployParams(deploymentId); + + if (!deployParams || !deployParams.privateKey) { + return res.status(400).json({ + success: false, + error: 'Приватный ключ не найден в параметрах деплоя' + }); + } + + // Исполняем в целевой сети + const result = await executeProposalInChain( + dleAddress, + proposalId, + targetChainId, + deployParams.privateKey, + userAddress + ); + + res.json({ + success: true, + data: { + proposalId, + targetChainId, + transactionHash: result.transactionHash, + blockNumber: result.blockNumber + } + }); + + } catch (error) { + console.error('[DLE Multichain] Ошибка при исполнении в целевой сети:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при исполнении в целевой сети: ' + error.message + }); + } +}); + +/** + * Вспомогательная функция для исполнения предложения в конкретной сети + */ +async function executeProposalInChain(dleAddress, proposalId, chainId, privateKey, userAddress) { + // Получаем RPC URL для целевой сети + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + if (!rpcUrl) { + throw new Error(`RPC URL для сети ${chainId} не найден`); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + + const dleAbi = [ + "function executeProposalBySignatures(uint256 _proposalId, address[] calldata signers, bytes[] calldata signatures) external" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, wallet); + + // Для простоты используем подпись от одного адреса (кошелька с приватным ключом) + // В реальности нужно собрать подписи от держателей токенов + const signers = [wallet.address]; + const signatures = []; // TODO: Реализовать сбор подписей + + // Временная заглушка - используем прямое исполнение если это возможно + // В реальности нужно реализовать сбор подписей от держателей токенов + try { + // Пытаемся исполнить напрямую (если это сеть голосования) + const directExecuteAbi = [ + "function executeProposal(uint256 _proposalId) external" + ]; + + const directDle = new ethers.Contract(dleAddress, directExecuteAbi, wallet); + const tx = await directDle.executeProposal(proposalId); + const receipt = await tx.wait(); + + return { + transactionHash: receipt.hash, + blockNumber: receipt.blockNumber + }; + + } catch (directError) { + // Если прямое исполнение невозможно, используем подписи + if (signatures.length === 0) { + throw new Error('Необходимо собрать подписи от держателей токенов для исполнения в целевой сети'); + } + + const tx = await dle.executeProposalBySignatures(proposalId, signers, signatures); + const receipt = await tx.wait(); + + return { + transactionHash: receipt.hash, + blockNumber: receipt.blockNumber + }; + } +} + +module.exports = router; + + diff --git a/backend/routes/dleProposals.js b/backend/routes/dleProposals.js index 96d3aef..ddf82c6 100644 --- a/backend/routes/dleProposals.js +++ b/backend/routes/dleProposals.js @@ -29,137 +29,297 @@ router.post('/get-proposals', async (req, res) => { console.log(`[DLE Proposals] Получение списка предложений для DLE: ${dleAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); - if (!rpcUrl) { - return res.status(500).json({ - success: false, - error: 'RPC URL для Sepolia не найден' - }); - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - - // ABI для чтения предложений (используем правильные функции из смарт-контракта) - const dleAbi = [ - "function getProposalState(uint256 _proposalId) external view returns (uint8 state)", - "function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)", - "function proposals(uint256) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, bytes memory operation, uint256 governanceChainId, uint256 snapshotTimepoint)", - "function quorumPercentage() external view returns (uint256)", - "function getPastTotalSupply(uint256 timepoint) external view returns (uint256)", - "event ProposalCreated(uint256 proposalId, address initiator, string description)" - ]; - - const dle = new ethers.Contract(dleAddress, dleAbi, provider); - - // Получаем события ProposalCreated для определения количества предложений - const currentBlock = await provider.getBlockNumber(); - const fromBlock = Math.max(0, currentBlock - 10000); // Последние 10000 блоков - - const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock); - - console.log(`[DLE Proposals] Найдено событий ProposalCreated: ${events.length}`); - console.log(`[DLE Proposals] Диапазон блоков: ${fromBlock} - ${currentBlock}`); - - const proposals = []; - - // Читаем информацию о каждом предложении - for (let i = 0; i < events.length; i++) { + // Получаем поддерживаемые сети DLE из контракта + let supportedChains = []; + try { + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + try { - const proposalId = events[i].args.proposalId; - console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`); + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + console.log(`[DLE Proposals] Не удалось найти сеть для адреса ${dleAddress}`); + // Fallback к известным сетям + supportedChains = [11155111, 17000, 421614, 84532]; + console.log(`[DLE Proposals] Используем fallback сети:`, supportedChains); + return; + } + if (rpcUrl) { + const provider = new ethers.JsonRpcProvider(rpcUrl); + const dleAbi = [ + "function getSupportedChainCount() external view returns (uint256)", + "function getSupportedChainId(uint256 _index) external view returns (uint256)" + ]; + const dle = new ethers.Contract(dleAddress, dleAbi, provider); - // Пробуем несколько раз для новых предложений - let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired; - let retryCount = 0; - const maxRetries = 1; + const chainCount = await dle.getSupportedChainCount(); + console.log(`[DLE Proposals] Количество поддерживаемых сетей: ${chainCount}`); - while (retryCount < maxRetries) { + for (let i = 0; i < Number(chainCount); i++) { + const chainId = await dle.getSupportedChainId(i); + supportedChains.push(Number(chainId)); + } + + console.log(`[DLE Proposals] Поддерживаемые сети из контракта:`, supportedChains); + } + } catch (error) { + console.log(`[DLE Proposals] Ошибка получения поддерживаемых сетей из контракта:`, error.message); + // Fallback к известным сетям + supportedChains = [11155111, 17000, 421614, 84532]; + console.log(`[DLE Proposals] Используем fallback сети:`, supportedChains); + } + + const allProposals = []; + + // Ищем предложения во всех поддерживаемых сетях + for (const chainId of supportedChains) { + try { + console.log(`[DLE Proposals] Поиск предложений в сети ${chainId}...`); + + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId); + if (!rpcUrl) { + console.log(`[DLE Proposals] RPC URL для сети ${chainId} не найден, пропускаем`); + continue; + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // ABI для чтения предложений (используем getProposalSummary для мультиконтрактов) + const dleAbi = [ + "function getProposalState(uint256 _proposalId) external view returns (uint8 state)", + "function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)", + "function getProposalSummary(uint256 _proposalId) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, uint256 governanceChainId, uint256 snapshotTimepoint, uint256[] memory targetChains)", + "function quorumPercentage() external view returns (uint256)", + "function getPastTotalSupply(uint256 timepoint) external view returns (uint256)", + "function totalSupply() external view returns (uint256)", + "event ProposalCreated(uint256 proposalId, address initiator, string description)" + ]; + + const dle = new ethers.Contract(dleAddress, dleAbi, provider); + + // Получаем события ProposalCreated для определения количества предложений + const currentBlock = await provider.getBlockNumber(); + const fromBlock = Math.max(0, currentBlock - 10000); // Последние 10000 блоков + + const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock); + + console.log(`[DLE Proposals] Найдено событий ProposalCreated в сети ${chainId}: ${events.length}`); + console.log(`[DLE Proposals] Диапазон блоков: ${fromBlock} - ${currentBlock}`); + + // Читаем информацию о каждом предложении + for (let i = 0; i < events.length; i++) { try { - proposalState = await dle.getProposalState(proposalId); - const result = await dle.checkProposalResult(proposalId); - isPassed = result.passed; - quorumReached = result.quorumReached; + const proposalId = events[i].args.proposalId; + console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`); + + // Пробуем несколько раз для новых предложений + let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired, currentTotalSupply, quorumPct; + let retryCount = 0; + const maxRetries = 1; + + while (retryCount < maxRetries) { + try { + proposalState = await dle.getProposalState(proposalId); + const result = await dle.checkProposalResult(proposalId); + isPassed = result.passed; + quorumReached = result.quorumReached; + + // Получаем данные о голосах из структуры Proposal (включая мультиконтрактные поля) + try { + const proposalData = await dle.getProposalSummary(proposalId); + forVotes = Number(proposalData.forVotes); + againstVotes = Number(proposalData.againstVotes); + + // Вычисляем требуемый кворум + quorumPct = Number(await dle.quorumPercentage()); + const pastSupply = Number(await dle.getPastTotalSupply(proposalData.snapshotTimepoint)); + quorumRequired = Math.floor((pastSupply * quorumPct) / 100); + + // Получаем текущий totalSupply для отображения + currentTotalSupply = Number(await dle.totalSupply()); + + console.log(`[DLE Proposals] Кворум для предложения ${proposalId}:`, { + quorumPercentage: quorumPct, + pastSupply: pastSupply, + quorumRequired: quorumRequired, + quorumPercentageFormatted: `${quorumPct}%`, + snapshotTimepoint: proposalData.snapshotTimepoint, + pastSupplyFormatted: `${(pastSupply / 10**18).toFixed(2)} DLE`, + quorumRequiredFormatted: `${(quorumRequired / 10**18).toFixed(2)} DLE` + }); + } catch (voteError) { + console.log(`[DLE Proposals] Ошибка получения голосов для предложения ${proposalId}:`, voteError.message); + forVotes = 0; + againstVotes = 0; + quorumRequired = 0; + currentTotalSupply = 0; + quorumPct = 0; + } + + break; // Успешно прочитали + } catch (error) { + retryCount++; + console.log(`[DLE Proposals] Попытка ${retryCount} чтения предложения ${proposalId} не удалась:`, error.message); + if (retryCount < maxRetries) { + await new Promise(resolve => setTimeout(resolve, 2000)); // Ждем 2 секунды + } else { + throw error; // Превышено количество попыток + } + } + } + + console.log(`[DLE Proposals] Данные предложения ${proposalId}:`, { + id: Number(proposalId), + description: events[i].args.description, + state: Number(proposalState), + isPassed: isPassed, + quorumReached: quorumReached, + forVotes: Number(forVotes), + againstVotes: Number(againstVotes), + quorumRequired: Number(quorumRequired), + initiator: events[i].args.initiator + }); + + // Фильтруем предложения по времени - только за последние 30 дней + const block = await provider.getBlock(events[i].blockNumber); + const proposalTime = block.timestamp; + const currentTime = Math.floor(Date.now() / 1000); + const thirtyDaysAgo = currentTime - (30 * 24 * 60 * 60); // 30 дней назад + + if (proposalTime < thirtyDaysAgo) { + console.log(`[DLE Proposals] Пропускаем старое предложение ${proposalId} (${new Date(proposalTime * 1000).toISOString()})`); + continue; + } + + // Показываем все предложения, включая выполненные и отмененные + // Согласно контракту: 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution + // Убрали фильтрацию выполненных и отмененных предложений для отображения в UI + + // Создаем уникальный ID, включающий chainId + const uniqueId = `${chainId}-${proposalId}`; + + // Получаем мультиконтрактные данные из proposalData (если доступны) + let operation = null; + let governanceChainId = null; + let targetChains = []; + let decodedOperation = null; + let operationDescription = null; - // Получаем данные о голосах из структуры Proposal try { - const proposalData = await dle.proposals(proposalId); - forVotes = Number(proposalData.forVotes); - againstVotes = Number(proposalData.againstVotes); + const proposalData = await dle.getProposalSummary(proposalId); + governanceChainId = Number(proposalData.governanceChainId); + targetChains = proposalData.targetChains.map(chain => Number(chain)); - // Вычисляем требуемый кворум - const quorumPct = Number(await dle.quorumPercentage()); - const pastSupply = Number(await dle.getPastTotalSupply(proposalData.snapshotTimepoint)); - quorumRequired = Math.floor((pastSupply * quorumPct) / 100); - } catch (voteError) { - console.log(`[DLE Proposals] Ошибка получения голосов для предложения ${proposalId}:`, voteError.message); - forVotes = 0; - againstVotes = 0; - quorumRequired = 0; + // Получаем operation из отдельного вызова (если нужно) + // operation не возвращается в getProposalSummary, но это не критично для мультиконтрактов + operation = null; // Пока не реализовано + + // Декодируем операцию (если доступна) + if (operation && operation !== '0x') { + const { decodeOperation, formatOperation } = require('../utils/operationDecoder'); + decodedOperation = decodeOperation(operation); + operationDescription = formatOperation(decodedOperation); + } + } catch (error) { + console.log(`[DLE Proposals] Не удалось получить мультиконтрактные данные для предложения ${proposalId}:`, error.message); + } + + const proposalInfo = { + id: Number(proposalId), + uniqueId: uniqueId, + description: events[i].args.description, + state: Number(proposalState), + isPassed: isPassed, + quorumReached: quorumReached, + forVotes: Number(forVotes), + againstVotes: Number(againstVotes), + quorumRequired: Number(quorumRequired), + totalSupply: Number(currentTotalSupply || 0), // Добавляем totalSupply + contractQuorumPercentage: Number(quorumPct), // Добавляем процент кворума из контракта + initiator: events[i].args.initiator, + blockNumber: events[i].blockNumber, + transactionHash: events[i].transactionHash, + chainId: chainId, // Добавляем информацию о сети + timestamp: proposalTime, + createdAt: new Date(proposalTime * 1000).toISOString(), + executed: Number(proposalState) === 3, // 3 = Executed + canceled: Number(proposalState) === 4, // 4 = Canceled + // Мультиконтрактные поля + operation: operation, + governanceChainId: governanceChainId, + targetChains: targetChains, + isMultichain: targetChains && targetChains.length > 0, + decodedOperation: decodedOperation, + operationDescription: operationDescription + }; + + // Проверяем, нет ли уже такого предложения (по уникальному ID) + const existingProposal = allProposals.find(p => p.uniqueId === uniqueId); + if (!existingProposal) { + allProposals.push(proposalInfo); + } else { + console.log(`[DLE Proposals] Пропускаем дубликат предложения ${uniqueId}`); + } + } catch (error) { + console.log(`[DLE Proposals] Ошибка при чтении предложения ${i}:`, error.message); + + // Если это ошибка декодирования, возможно предложение еще не полностью записано + if (error.message.includes('could not decode result data')) { + console.log(`[DLE Proposals] Предложение ${i} еще не полностью синхронизировано, пропускаем`); + continue; } - break; // Успешно прочитали - } catch (error) { - retryCount++; - console.log(`[DLE Proposals] Попытка ${retryCount} чтения предложения ${proposalId} не удалась:`, error.message); - if (retryCount < maxRetries) { - await new Promise(resolve => setTimeout(resolve, 2000)); // Ждем 2 секунды - } else { - throw error; // Превышено количество попыток - } + // Продолжаем с следующим предложением } } - console.log(`[DLE Proposals] Данные предложения ${proposalId}:`, { - id: Number(proposalId), - description: events[i].args.description, - state: Number(proposalState), - isPassed: isPassed, - quorumReached: quorumReached, - forVotes: Number(forVotes), - againstVotes: Number(againstVotes), - quorumRequired: Number(quorumRequired), - initiator: events[i].args.initiator - }); + console.log(`[DLE Proposals] Найдено предложений в сети ${chainId}: ${events.length}`); - const proposalInfo = { - id: Number(proposalId), - description: events[i].args.description, - state: Number(proposalState), - isPassed: isPassed, - quorumReached: quorumReached, - forVotes: Number(forVotes), - againstVotes: Number(againstVotes), - quorumRequired: Number(quorumRequired), - initiator: events[i].args.initiator, - blockNumber: events[i].blockNumber, - transactionHash: events[i].transactionHash - }; - - proposals.push(proposalInfo); } catch (error) { - console.log(`[DLE Proposals] Ошибка при чтении предложения ${i}:`, error.message); - - // Если это ошибка декодирования, возможно предложение еще не полностью записано - if (error.message.includes('could not decode result data')) { - console.log(`[DLE Proposals] Предложение ${i} еще не полностью синхронизировано, пропускаем`); - continue; - } - - // Продолжаем с следующим предложением + console.log(`[DLE Proposals] Ошибка при поиске предложений в сети ${chainId}:`, error.message); + // Продолжаем с следующей сетью } } - // Сортируем по ID предложения (новые сверху) - proposals.sort((a, b) => b.id - a.id); + // Сортируем по времени создания (новые сверху), затем по ID + allProposals.sort((a, b) => { + if (a.timestamp !== b.timestamp) { + return b.timestamp - a.timestamp; + } + return b.id - a.id; + }); - console.log(`[DLE Proposals] Найдено предложений: ${proposals.length}`); + console.log(`[DLE Proposals] Найдено предложений: ${allProposals.length}`); res.json({ success: true, data: { - proposals: proposals, - totalCount: proposals.length + proposals: allProposals, + totalCount: allProposals.length } }); @@ -186,8 +346,41 @@ router.post('/get-proposal-info', async (req, res) => { console.log(`[DLE Proposals] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`); - // Получаем RPC URL для Sepolia - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -864,6 +1057,9 @@ router.post('/vote-proposal', async (req, res) => { const dle = new ethers.Contract(dleAddress, dleAbi, provider); + // Пропускаем проверку hasVoted - функция не существует в контракте + console.log(`[DLE Proposals] Пропускаем проверку hasVoted - полагаемся на смарт-контракт`); + // Подготавливаем данные для транзакции (не отправляем) const txData = await dle.vote.populateTransaction(proposalId, support); @@ -889,6 +1085,53 @@ router.post('/vote-proposal', async (req, res) => { } }); +// Проверить статус голосования пользователя +router.post('/check-vote-status', async (req, res) => { + try { + const { dleAddress, proposalId, voterAddress } = req.body; + + if (!dleAddress || proposalId === undefined || !voterAddress) { + return res.status(400).json({ + success: false, + error: 'Необходимы dleAddress, proposalId и voterAddress' + }); + } + + console.log(`[DLE Proposals] Проверка статуса голосования для ${voterAddress} по предложению ${proposalId} в DLE: ${dleAddress}`); + + const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'RPC URL для Sepolia не найден' + }); + } + + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // Функция hasVoted не существует в контракте DLE + console.log(`[DLE Proposals] Функция hasVoted не поддерживается в контракте DLE`); + + const hasVoted = false; // Всегда возвращаем false, так как функция не существует + + res.json({ + success: true, + data: { + hasVoted: hasVoted, + voterAddress: voterAddress, + proposalId: proposalId + } + }); + + } catch (error) { + console.error('[DLE Proposals] Ошибка при проверке статуса голосования:', error); + res.status(500).json({ + success: false, + error: 'Ошибка при проверке статуса голосования: ' + error.message + }); + } +}); + // Endpoint для отслеживания подтверждения транзакций голосования router.post('/track-vote-transaction', async (req, res) => { try { diff --git a/backend/routes/dleTokens.js b/backend/routes/dleTokens.js index d8e976d..5509b57 100644 --- a/backend/routes/dleTokens.js +++ b/backend/routes/dleTokens.js @@ -29,7 +29,41 @@ router.post('/get-token-balance', async (req, res) => { console.log(`[DLE Tokens] Получение баланса токенов для аккаунта: ${account} в DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -81,7 +115,41 @@ router.post('/get-total-supply', async (req, res) => { console.log(`[DLE Tokens] Получение общего предложения токенов для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, @@ -132,7 +200,41 @@ router.post('/get-token-holders', async (req, res) => { console.log(`[DLE Tokens] Получение держателей токенов для DLE: ${dleAddress}`); - const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111); + // Определяем корректную сеть для данного адреса + let rpcUrl, targetChainId; + let candidateChainIds = [17000, 11155111, 421614, 84532]; // Fallback + + try { + // Получаем поддерживаемые сети из параметров деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + candidateChainIds = params.supportedChainIds || candidateChainIds; + } + } catch (error) { + console.error('❌ Ошибка получения параметров деплоя, используем fallback:', error); + } + + for (const cid of candidateChainIds) { + try { + const url = await rpcProviderService.getRpcUrlByChainId(cid); + if (!url) continue; + const prov = new ethers.JsonRpcProvider(url); + const code = await prov.getCode(dleAddress); + if (code && code !== '0x') { + rpcUrl = url; + targetChainId = cid; + break; + } + } catch (_) {} + } + + if (!rpcUrl) { + return res.status(500).json({ + success: false, + error: 'Не удалось найти сеть, где по адресу есть контракт' + }); + } if (!rpcUrl) { return res.status(500).json({ success: false, diff --git a/backend/routes/dleV2.js b/backend/routes/dleV2.js index 1e0f68b..d819e2f 100644 --- a/backend/routes/dleV2.js +++ b/backend/routes/dleV2.js @@ -12,8 +12,8 @@ const express = require('express'); const router = express.Router(); -const DLEV2Service = require('../services/dleV2Service'); -const dleV2Service = new DLEV2Service(); +const UnifiedDeploymentService = require('../services/unifiedDeploymentService'); +const unifiedDeploymentService = new UnifiedDeploymentService(); const logger = require('../utils/logger'); const auth = require('../middleware/auth'); const path = require('path'); @@ -38,7 +38,7 @@ async function executeDeploymentInBackground(deploymentId, dleParams) { deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта', 'info'); // Выполняем деплой с передачей deploymentId для WebSocket обновлений - const result = await dleV2Service.createDLE(dleParams, deploymentId); + const result = await unifiedDeploymentService.createDLE(dleParams, deploymentId); // Завершаем успешно deploymentTracker.completeDeployment(deploymentId, result.data); @@ -114,7 +114,7 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => */ router.get('/', async (req, res, next) => { try { - const dles = dleV2Service.getAllDLEs(); + const dles = await unifiedDeploymentService.getAllDeployments(); res.json({ success: true, @@ -490,13 +490,8 @@ router.get('/verify/status/:address', auth.requireAuth, async (req, res) => { router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { address } = req.params; - let { etherscanApiKey } = req.body || {}; - if (!etherscanApiKey) { - try { - const { getSecret } = require('../services/secretStore'); - etherscanApiKey = await getSecret('ETHERSCAN_V2_API_KEY'); - } catch(_) {} - } + const ApiKeyManager = require('../utils/apiKeyManager'); + const etherscanApiKey = ApiKeyManager.getEtherscanApiKey({}, req.body); const data = verificationStore.read(address); if (!data || !data.chains) return res.json({ success: true, data }); @@ -504,7 +499,7 @@ router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, asy const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || '')); if (needResubmit && etherscanApiKey) { // Найти карточку DLE - const list = dleV2Service.getAllDLEs(); + const list = unifiedDeploymentService.getAllDLEs(); const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase()); if (card) { const deployParams = { @@ -520,11 +515,11 @@ router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, asy initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [], initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [], supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []), - currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1 + currentChainId: card.governanceSettings?.currentChainId || 1 // governance chain, не первая сеть }; const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } }; try { - await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); + await unifiedDeploymentService.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); } catch (_) {} } } @@ -552,12 +547,14 @@ router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, asy router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => { try { const { address } = req.params; - const { etherscanApiKey } = req.body || {}; - if (!etherscanApiKey && !process.env.ETHERSCAN_API_KEY) { + const ApiKeyManager = require('../utils/apiKeyManager'); + const etherscanApiKey = ApiKeyManager.getEtherscanApiKey({}, req.body); + + if (!etherscanApiKey) { return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' }); } // Найти карточку DLE по адресу - const list = dleV2Service.getAllDLEs(); + const list = unifiedDeploymentService.getAllDLEs(); const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase()); if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' }); @@ -575,13 +572,13 @@ router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, as initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [], initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [], supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []), - currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1 + currentChainId: card.governanceSettings?.currentChainId || 1 // governance chain, не первая сеть }; // Сформировать deployResult из карточки const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } }; - await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); + await unifiedDeploymentService.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); const updated = verificationStore.read(address); return res.json({ success: true, data: updated }); } catch (e) { @@ -597,7 +594,7 @@ router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) = if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) { return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' }); } - const result = await dleV2Service.checkBalances(supportedChainIds, privateKey); + const result = await unifiedDeploymentService.checkBalances(supportedChainIds, privateKey); return res.json({ success: true, data: result }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); diff --git a/backend/routes/moduleDeployment.js b/backend/routes/moduleDeployment.js index e64eb35..9eac4c6 100644 --- a/backend/routes/moduleDeployment.js +++ b/backend/routes/moduleDeployment.js @@ -60,8 +60,10 @@ router.post('/deploy-module-from-db', async (req, res) => { process.env.PRIVATE_KEY = params.privateKey || params.private_key; } - if (params.etherscanApiKey || params.etherscan_api_key) { - process.env.ETHERSCAN_API_KEY = params.etherscanApiKey || params.etherscan_api_key; + const ApiKeyManager = require('../utils/apiKeyManager'); + const etherscanKey = ApiKeyManager.getAndSetEtherscanApiKey(params); + + if (etherscanKey) { } // Запускаем деплой модулей через скрипт diff --git a/backend/scripts/clear-form-cache.js b/backend/scripts/clear-form-cache.js new file mode 100644 index 0000000..fc76df4 --- /dev/null +++ b/backend/scripts/clear-form-cache.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +/** + * Скрипт для очистки кэша формы деплоя + * Удаляет localStorage данные, чтобы форма использовала свежие данные + */ + +console.log('🧹 Очистка кэша формы деплоя...'); + +// Инструкции для пользователя +console.log(` +📋 ИНСТРУКЦИИ ДЛЯ ОЧИСТКИ КЭША: + +1. Откройте браузер и перейдите на http://localhost:5173 +2. Откройте Developer Tools (F12) +3. Перейдите во вкладку "Application" или "Storage" +4. Найдите "Local Storage" -> "http://localhost:5173" +5. Найдите ключ "dle_form_data" и удалите его +6. Или выполните в консоли браузера: + localStorage.removeItem('dle_form_data'); +7. Перезагрузите страницу (F5) + +🔧 АЛЬТЕРНАТИВНО - можно добавить кнопку "Очистить кэш" в форму: + - Добавить кнопку в DleDeployFormView.vue + - При клике выполнять: localStorage.removeItem('dle_form_data'); + - Перезагружать страницу + +✅ После очистки кэша форма будет использовать свежие данные +`); + +console.log('🏁 Инструкции выведены. Выполните очистку кэша в браузере.'); diff --git a/backend/scripts/contracts-data/modules-deploy-summary.json b/backend/scripts/contracts-data/modules-deploy-summary.json deleted file mode 100644 index cfcd0a1..0000000 --- a/backend/scripts/contracts-data/modules-deploy-summary.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "deploymentId": "modules-deploy-1758801398489", - "dleAddress": "0x40A99dBEC8D160a226E856d370dA4f3C67713940", - "dleName": "DLE Test", - "dleSymbol": "TOKEN", - "dleLocation": "101000, Москва, Москва, Тверская, 1, 101", - "dleJurisdiction": 643, - "dleCoordinates": "55.7614035,37.6342935", - "dleOktmo": "45000000", - "dleOkvedCodes": [ - "62.01", - "63.11" - ], - "dleKpp": "773009001", - "dleLogoURI": "/uploads/logos/default-token.svg", - "dleSupportedChainIds": [ - 11155111, - 17000, - 421614, - 84532 - ], - "totalNetworks": 4, - "successfulNetworks": 4, - "modulesDeployed": [ - "reader" - ], - "networks": [ - { - "chainId": 17000, - "rpcUrl": "https://ethereum-holesky.publicnode.com", - "modules": [ - { - "type": "reader", - "address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545", - "success": true, - "verification": "success" - } - ] - }, - { - "chainId": 84532, - "rpcUrl": "https://sepolia.base.org", - "modules": [ - { - "type": "reader", - "address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545", - "success": true, - "verification": "success" - } - ] - }, - { - "chainId": 421614, - "rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc", - "modules": [ - { - "type": "reader", - "address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545", - "success": true, - "verification": "success" - } - ] - }, - { - "chainId": 11155111, - "rpcUrl": "https://1rpc.io/sepolia", - "modules": [ - { - "type": "reader", - "address": "0x1bA03A5f814d3781984D0f7Bca0E8E74c5e47545", - "success": true, - "verification": "success" - } - ] - } - ], - "timestamp": "2025-09-25T11:56:38.490Z" -} \ No newline at end of file diff --git a/backend/scripts/deploy/deploy-modules.js b/backend/scripts/deploy/deploy-modules.js index b962fa4..80e813e 100644 --- a/backend/scripts/deploy/deploy-modules.js +++ b/backend/scripts/deploy/deploy-modules.js @@ -14,27 +14,13 @@ const hre = require('hardhat'); const path = require('path'); const fs = require('fs'); +const logger = require('../../utils/logger'); +const { getFeeOverrides, createProviderAndWallet, alignNonce, getNetworkInfo, createRPCConnection, sendTransactionWithRetry } = require('../../utils/deploymentUtils'); +const { nonceManager } = require('../../utils/nonceManager'); // WebSocket сервис для отслеживания деплоя const deploymentWebSocketService = require('../../services/deploymentWebSocketService'); -// Подбираем безопасные gas/fee для разных сетей (включая L2) -async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { - const fee = await provider.getFeeData(); - const overrides = {}; - const minPriority = (await (async () => hre.ethers.parseUnits(minPriorityGwei.toString(), 'gwei'))()); - const minFee = (await (async () => hre.ethers.parseUnits(minFeeGwei.toString(), 'gwei'))()); - if (fee.maxFeePerGas) { - overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas; - overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n) - ? fee.maxPriorityFeePerGas - : minPriority; - } else if (fee.gasPrice) { - overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice; - } - return overrides; -} - // Конфигурация модулей для деплоя const MODULE_CONFIGS = { treasury: { @@ -88,22 +74,29 @@ const MODULE_CONFIGS = { // Деплой модуля в одной сети с CREATE2 async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce, moduleInit, moduleType) { const { ethers } = hre; - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(pk, provider); - const net = await provider.getNetwork(); - - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType}...`); - // 1) Выравнивание nonce до targetNonce нулевыми транзакциями (если нужно) - let current = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetNonce}`); + // Используем новый менеджер RPC с retry логикой + const { provider, wallet, network } = await createRPCConnection(rpcUrl, pk, { + maxRetries: 3, + timeout: 30000 + }); + + const net = network; + + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType}...`); + + // 1) Используем NonceManager для правильного управления nonce + const { nonceManager } = require('../../utils/nonceManager'); + const chainId = Number(net.chainId); + let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId); + logger.info(`[MODULES_DBG] chainId=${chainId} current nonce=${current} target=${targetNonce}`); if (current > targetNonce) { throw new Error(`Current nonce ${current} > targetNonce ${targetNonce} on chainId=${Number(net.chainId)}`); } if (current < targetNonce) { - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetNonce} (${targetNonce - current} transactions needed)`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetNonce} (${targetNonce - current} transactions needed)`); // Используем burn address для более надежных транзакций const burnAddress = "0x000000000000000000000000000000000000dEaD"; @@ -123,15 +116,14 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce gasLimit, ...overrides }; - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`); - const txFill = await wallet.sendTransaction(txReq); - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); - await txFill.wait(); - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`); + const { tx: txFill, receipt } = await sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 }); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); sent = true; } catch (e) { lastErr = e; - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`); if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) { gasLimit = 50000; @@ -139,8 +131,17 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce } if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) { + // Сбрасываем кэш и получаем актуальный nonce + const { nonceManager } = require('../../utils/nonceManager'); + nonceManager.resetNonce(wallet.address, Number(net.chainId)); current = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`); + + // Если новый nonce больше целевого, это критическая ошибка + if (current > targetNonce) { + throw new Error(`Current nonce ${current} > target nonce ${targetNonce} on chainId=${Number(net.chainId)}. Cannot proceed with module deployment.`); + } + continue; } @@ -149,20 +150,20 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce } if (!sent) { - console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); + logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); throw lastErr || new Error('filler tx failed'); } current++; } - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); } else { - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); } // 2) Деплой модуля напрямую на согласованном nonce - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType} directly with nonce=${targetNonce}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType} directly with nonce=${targetNonce}`); const feeOverrides = await getFeeOverrides(provider); let gasLimit; @@ -179,7 +180,7 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce const fallbackGas = maxByBalance > 2_000_000n ? 2_000_000n : (maxByBalance < 500_000n ? 500_000n : maxByBalance); gasLimit = est ? (est + est / 5n) : fallbackGas; - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`); } catch (_) { gasLimit = 1_000_000n; } @@ -189,41 +190,115 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce from: wallet.address, nonce: targetNonce }); - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} predicted ${moduleType} address=${predictedAddress}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} predicted ${moduleType} address=${predictedAddress}`); // Проверяем, не развернут ли уже контракт const existingCode = await provider.getCode(predictedAddress); if (existingCode && existingCode !== '0x') { - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} already exists at predictedAddress, skip deploy`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} already exists at predictedAddress, skip deploy`); return { address: predictedAddress, chainId: Number(net.chainId) }; } - // Деплоим модуль + // Деплоим модуль с retry логикой для обработки race conditions let tx; - try { - tx = await wallet.sendTransaction({ - data: moduleInit, - nonce: targetNonce, - gasLimit, - ...feeOverrides - }); - } catch (e) { - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`); - // Повторная попытка с обновленным nonce - const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`); - tx = await wallet.sendTransaction({ - data: moduleInit, - nonce: updatedNonce, - gasLimit, - ...feeOverrides - }); + let deployAttempts = 0; + const maxDeployAttempts = 5; + + while (deployAttempts < maxDeployAttempts) { + try { + deployAttempts++; + + // Получаем актуальный nonce прямо перед отправкой транзакции + const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, Number(net.chainId), { timeout: 15000, maxRetries: 5 }); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts}/${maxDeployAttempts} with current nonce=${currentNonce} (target was ${targetNonce})`); + + const txData = { + data: moduleInit, + nonce: currentNonce, + gasLimit, + ...feeOverrides + }; + + const result = await sendTransactionWithRetry(wallet, txData, { maxRetries: 3 }); + tx = result.tx; + + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy successful on attempt ${deployAttempts}`); + break; // Успешно отправили, выходим из цикла + + } catch (e) { + const errorMsg = e?.message || e; + logger.warn(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts} failed: ${errorMsg}`); + + // Проверяем, является ли это ошибкой nonce + if (String(errorMsg).toLowerCase().includes('nonce too low') && deployAttempts < maxDeployAttempts) { + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} nonce race condition detected, retrying...`); + + // Получаем актуальный nonce из сети + const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, Number(net.chainId), { timeout: 15000, maxRetries: 5 }); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce: ${currentNonce}, target: ${targetNonce}`); + + // Если текущий nonce больше целевого, обновляем targetNonce + if (currentNonce > targetNonce) { + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} current nonce ${currentNonce} > target nonce ${targetNonce}, updating target`); + targetNonce = currentNonce; + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated targetNonce to: ${targetNonce}`); + + // Короткая задержка перед следующей попыткой + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + + // Если текущий nonce меньше целевого, выравниваем его + if (currentNonce < targetNonce) { + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} aligning nonce from ${currentNonce} to ${targetNonce}`); + + // Выравниваем nonce нулевыми транзакциями + for (let i = currentNonce; i < targetNonce; i++) { + try { + const fillerTx = await wallet.sendTransaction({ + to: '0x000000000000000000000000000000000000dEaD', + value: 0, + gasLimit: 21000, + nonce: i, + ...feeOverrides + }); + + await fillerTx.wait(); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx ${i} confirmed`); + + // Обновляем nonce в кэше + nonceManager.reserveNonce(wallet.address, Number(net.chainId), i); + + } catch (fillerError) { + logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx ${i} failed: ${fillerError.message}`); + throw fillerError; + } + } + } + + // ВАЖНО: Обновляем targetNonce на актуальный nonce для следующей попытки + targetNonce = currentNonce; + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated targetNonce to: ${targetNonce}`); + + // Короткая задержка перед следующей попыткой + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + + // Если это не ошибка nonce или исчерпаны попытки, выбрасываем ошибку + if (deployAttempts >= maxDeployAttempts) { + throw new Error(`Module deployment failed after ${maxDeployAttempts} attempts: ${errorMsg}`); + } + + // Для других ошибок делаем короткую задержку и пробуем снова + await new Promise(resolve => setTimeout(resolve, 2000)); + } } const rc = await tx.wait(); const deployedAddress = rc.contractAddress || predictedAddress; - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployed at=${deployedAddress}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployed at=${deployedAddress}`); return { address: deployedAddress, chainId: Number(net.chainId) }; } @@ -231,11 +306,16 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce // Деплой всех модулей в одной сети async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) { const { ethers } = hre; - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(pk, provider); - const net = await provider.getNetwork(); + + // Используем новый менеджер RPC с retry логикой + const { provider, wallet, network } = await createRPCConnection(rpcUrl, pk, { + maxRetries: 3, + timeout: 30000 + }); + + const net = network; - console.log(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying modules: ${modulesToDeploy.join(', ')}`); + logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying modules: ${modulesToDeploy.join(', ')}`); const results = {}; @@ -248,14 +328,14 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Деплой модуля ${moduleType} в сети ${net.name || net.chainId}`); if (!MODULE_CONFIGS[moduleType]) { - console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type: ${moduleType}`); + logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} Unknown module type: ${moduleType}`); results[moduleType] = { success: false, error: `Unknown module type: ${moduleType}` }; deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Неизвестный тип модуля: ${moduleType}`); continue; } if (!moduleInit) { - console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} No init code for module: ${moduleType}`); + logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} No init code for module: ${moduleType}`); results[moduleType] = { success: false, error: `No init code for module: ${moduleType}` }; deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Отсутствует код инициализации для модуля: ${moduleType}`); continue; @@ -266,7 +346,7 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo results[moduleType] = { ...result, success: true }; deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleType} успешно задеплоен в сети ${net.name || net.chainId}: ${result.address}`); } catch (error) { - console.error(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployment failed:`, error.message); + logger.error(`[MODULES_DBG] chainId=${Number(net.chainId)} ${moduleType} deployment failed:`, error.message); results[moduleType] = { chainId: Number(net.chainId), success: false, @@ -287,9 +367,10 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo async function deployAllModulesInAllNetworks(networks, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces) { const results = []; - for (let i = 0; i < networks.length; i++) { - const rpcUrl = networks[i]; - console.log(`[MODULES_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`); + for (let i = 0; i < connections.length; i++) { + const connection = connections[i]; + const rpcUrl = connection.rpcUrl; + logger.info(`[MODULES_DBG] deploying modules to network ${i + 1}/${connections.length}: ${rpcUrl}`); const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces); results.push(result); @@ -323,10 +404,10 @@ async function main() { // Проверяем, передан ли конкретный deploymentId const deploymentId = process.env.DEPLOYMENT_ID; if (deploymentId) { - console.log(`🔍 Ищем параметры для deploymentId: ${deploymentId}`); + logger.info(`🔍 Ищем параметры для deploymentId: ${deploymentId}`); params = await deployParamsService.getDeployParams(deploymentId); if (params) { - console.log('✅ Параметры загружены из базы данных по deploymentId'); + logger.info('✅ Параметры загружены из базы данных по deploymentId'); } else { throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`); } @@ -335,7 +416,7 @@ async function main() { const latestParams = await deployParamsService.getLatestDeployParams(1); if (latestParams.length > 0) { params = latestParams[0]; - console.log('✅ Параметры загружены из базы данных (последние)'); + logger.info('✅ Параметры загружены из базы данных (последние)'); } else { throw new Error('Параметры деплоя не найдены в базе данных'); } @@ -343,18 +424,11 @@ async function main() { await deployParamsService.close(); } catch (dbError) { - console.log('⚠️ Не удалось загрузить параметры из БД, пытаемся загрузить из файла:', dbError.message); - - // Fallback к файлу - const paramsPath = path.join(__dirname, './current-params.json'); - if (!fs.existsSync(paramsPath)) { - throw new Error('Файл параметров не найден: ' + paramsPath); - } - - params = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); - console.log('✅ Параметры загружены из файла'); + logger.error('❌ Критическая ошибка: не удалось загрузить параметры из БД:', dbError.message); + logger.error('❌ Система должна использовать только базу данных для хранения параметров деплоя'); + throw new Error(`Не удалось загрузить параметры деплоя из БД: ${dbError.message}. Система должна использовать только базу данных.`); } - console.log('[MODULES_DBG] Загружены параметры:', { + logger.info('[MODULES_DBG] Загружены параметры:', { name: params.name, symbol: params.symbol, supportedChainIds: params.supportedChainIds, @@ -370,13 +444,13 @@ async function main() { let modulesToDeploy; if (moduleTypeFromArgs) { modulesToDeploy = [moduleTypeFromArgs]; - console.log(`[MODULES_DBG] Деплой конкретного модуля: ${moduleTypeFromArgs}`); + logger.info(`[MODULES_DBG] Деплой конкретного модуля: ${moduleTypeFromArgs}`); } else if (params.modulesToDeploy && params.modulesToDeploy.length > 0) { modulesToDeploy = params.modulesToDeploy; - console.log(`[MODULES_DBG] Деплой модулей из БД: ${modulesToDeploy.join(', ')}`); + logger.info(`[MODULES_DBG] Деплой модулей из БД: ${modulesToDeploy.join(', ')}`); } else { modulesToDeploy = ['treasury', 'timelock', 'reader']; - console.log(`[MODULES_DBG] Деплой модулей по умолчанию: ${modulesToDeploy.join(', ')}`); + logger.info(`[MODULES_DBG] Деплой модулей по умолчанию: ${modulesToDeploy.join(', ')}`); } if (!pk) throw new Error('PRIVATE_KEY not found in params or environment'); @@ -384,11 +458,11 @@ async function main() { if (!salt) throw new Error('CREATE2_SALT not found in params'); if (networks.length === 0) throw new Error('RPC URLs not found in params'); - console.log(`[MODULES_DBG] Starting modules deployment to ${networks.length} networks`); - console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`); - console.log(`[MODULES_DBG] Modules to deploy: ${modulesToDeploy.join(', ')}`); - console.log(`[MODULES_DBG] Networks:`, networks); - console.log(`[MODULES_DBG] Using private key from: ${params.privateKey ? 'database' : 'environment'}`); + logger.info(`[MODULES_DBG] Starting modules deployment to ${networks.length} networks`); + logger.info(`[MODULES_DBG] DLE Address: ${dleAddress}`); + logger.info(`[MODULES_DBG] Modules to deploy: ${modulesToDeploy.join(', ')}`); + logger.info(`[MODULES_DBG] Networks:`, networks); + logger.info(`[MODULES_DBG] Using private key from: ${params.privateKey ? 'database' : 'environment'}`); // Уведомляем WebSocket клиентов о начале деплоя if (moduleTypeFromArgs) { @@ -400,9 +474,11 @@ async function main() { } // Устанавливаем API ключ Etherscan из базы данных, если доступен - if (params.etherscanApiKey || params.etherscan_api_key) { - process.env.ETHERSCAN_API_KEY = params.etherscanApiKey || params.etherscan_api_key; - console.log(`[MODULES_DBG] Using Etherscan API key from database`); + const ApiKeyManager = require('../../utils/apiKeyManager'); + const etherscanKey = ApiKeyManager.getAndSetEtherscanApiKey(params); + + if (etherscanKey) { + logger.info(`[MODULES_DBG] Using Etherscan API key from database`); } // Проверяем, что все модули поддерживаются @@ -420,28 +496,43 @@ async function main() { const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName); // Получаем аргументы конструктора для первой сети (для расчета init кода) - const firstProvider = new hre.ethers.JsonRpcProvider(networks[0]); - const firstWallet = new hre.ethers.Wallet(pk, firstProvider); - const firstNetwork = await firstProvider.getNetwork(); + const firstConnection = await createRPCConnection(networks[0], pk, { + maxRetries: 3, + timeout: 30000 + }); + const firstProvider = firstConnection.provider; + const firstWallet = firstConnection.wallet; + const firstNetwork = firstConnection.network; // Получаем аргументы конструктора const constructorArgs = moduleConfig.constructorArgs(dleAddress, Number(firstNetwork.chainId), firstWallet.address); - console.log(`[MODULES_DBG] ${moduleType} constructor args:`, constructorArgs); + logger.info(`[MODULES_DBG] ${moduleType} constructor args:`, constructorArgs); const deployTx = await ContractFactory.getDeployTransaction(...constructorArgs); moduleInits[moduleType] = deployTx.data; moduleInitCodeHashes[moduleType] = ethers.keccak256(deployTx.data); - console.log(`[MODULES_DBG] ${moduleType} init code prepared, hash: ${moduleInitCodeHashes[moduleType]}`); + logger.info(`[MODULES_DBG] ${moduleType} init code prepared, hash: ${moduleInitCodeHashes[moduleType]}`); } // Подготовим провайдеры и вычислим общие nonce для каждого модуля - const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u)); - const wallets = providers.map(p => new hre.ethers.Wallet(pk, p)); + // Создаем RPC соединения с retry логикой + logger.info(`[MODULES_DBG] Создаем RPC соединения для ${networks.length} сетей...`); + const connections = await createMultipleRPCConnections(networks, pk, { + maxRetries: 3, + timeout: 30000 + }); + + if (connections.length === 0) { + throw new Error('Не удалось установить ни одного RPC соединения'); + } + + logger.info(`[MODULES_DBG] ✅ Успешно подключились к ${connections.length}/${networks.length} сетям`); + const nonces = []; - for (let i = 0; i < providers.length; i++) { - const n = await providers[i].getTransactionCount(wallets[i].address, 'pending'); + for (const connection of connections) { + const n = await nonceManager.getNonce(connection.wallet.address, connection.rpcUrl, connection.chainId); nonces.push(n); } @@ -454,48 +545,50 @@ async function main() { currentMaxNonce++; // каждый следующий модуль получает nonce +1 } - console.log(`[MODULES_DBG] nonces=${JSON.stringify(nonces)} targetNonces=${JSON.stringify(targetNonces)}`); + logger.info(`[MODULES_DBG] nonces=${JSON.stringify(nonces)} targetNonces=${JSON.stringify(targetNonces)}`); // ПАРАЛЛЕЛЬНЫЙ деплой всех модулей во всех сетях одновременно - console.log(`[MODULES_DBG] Starting PARALLEL deployment of all modules to ${networks.length} networks`); + logger.info(`[MODULES_DBG] Starting PARALLEL deployment of all modules to ${networks.length} networks`); const deploymentPromises = networks.map(async (rpcUrl, networkIndex) => { - console.log(`[MODULES_DBG] 🚀 Starting deployment to network ${networkIndex + 1}/${networks.length}: ${rpcUrl}`); + logger.info(`[MODULES_DBG] 🚀 Starting deployment to network ${networkIndex + 1}/${networks.length}: ${rpcUrl}`); try { - // Получаем chainId динамически из сети - const provider = new hre.ethers.JsonRpcProvider(rpcUrl); - const network = await provider.getNetwork(); + // Получаем chainId динамически из сети с retry логикой + const { provider, network } = await createRPCConnection(rpcUrl, pk, { + maxRetries: 3, + timeout: 30000 + }); const chainId = Number(network.chainId); - console.log(`[MODULES_DBG] 📡 Network ${networkIndex + 1} chainId: ${chainId}`); + logger.info(`[MODULES_DBG] 📡 Network ${networkIndex + 1} chainId: ${chainId}`); const result = await deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesToDeploy, moduleInits, targetNonces); - console.log(`[MODULES_DBG] ✅ Network ${networkIndex + 1} (chainId: ${chainId}) deployment SUCCESS`); + logger.info(`[MODULES_DBG] ✅ Network ${networkIndex + 1} (chainId: ${chainId}) deployment SUCCESS`); return { rpcUrl, chainId, ...result }; } catch (error) { - console.error(`[MODULES_DBG] ❌ Network ${networkIndex + 1} deployment FAILED:`, error.message); + logger.error(`[MODULES_DBG] ❌ Network ${networkIndex + 1} deployment FAILED:`, error.message); return { rpcUrl, error: error.message }; } }); // Ждем завершения всех деплоев const deployResults = await Promise.all(deploymentPromises); - console.log(`[MODULES_DBG] All ${networks.length} deployments completed`); + logger.info(`[MODULES_DBG] All ${networks.length} deployments completed`); // Логируем результаты деплоя для каждой сети deployResults.forEach((result, index) => { if (result.modules) { - console.log(`[MODULES_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS`); + logger.info(`[MODULES_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS`); Object.entries(result.modules).forEach(([moduleType, moduleResult]) => { if (moduleResult.success) { - console.log(`[MODULES_DBG] ✅ ${moduleType}: ${moduleResult.address}`); + logger.info(`[MODULES_DBG] ✅ ${moduleType}: ${moduleResult.address}`); } else { - console.log(`[MODULES_DBG] ❌ ${moduleType}: ${moduleResult.error}`); + logger.info(`[MODULES_DBG] ❌ ${moduleType}: ${moduleResult.error}`); } }); } else { - console.log(`[MODULES_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`); + logger.info(`[MODULES_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`); } }); @@ -506,38 +599,38 @@ async function main() { .map(r => r.modules[moduleType].address); const uniqueAddresses = [...new Set(addresses)]; - console.log(`[MODULES_DBG] ${moduleType} addresses:`, addresses); - console.log(`[MODULES_DBG] ${moduleType} unique addresses:`, uniqueAddresses); + logger.info(`[MODULES_DBG] ${moduleType} addresses:`, addresses); + logger.info(`[MODULES_DBG] ${moduleType} unique addresses:`, uniqueAddresses); if (uniqueAddresses.length > 1) { - console.error(`[MODULES_DBG] ERROR: ${moduleType} addresses are different across networks!`); - console.error(`[MODULES_DBG] addresses:`, uniqueAddresses); + logger.error(`[MODULES_DBG] ERROR: ${moduleType} addresses are different across networks!`); + logger.error(`[MODULES_DBG] addresses:`, uniqueAddresses); throw new Error(`Nonce alignment failed for ${moduleType} - addresses are different`); } if (uniqueAddresses.length === 0) { - console.error(`[MODULES_DBG] ERROR: No successful ${moduleType} deployments!`); + logger.error(`[MODULES_DBG] ERROR: No successful ${moduleType} deployments!`); throw new Error(`No successful ${moduleType} deployments`); } - console.log(`[MODULES_DBG] SUCCESS: All ${moduleType} addresses are identical:`, uniqueAddresses[0]); + logger.info(`[MODULES_DBG] SUCCESS: All ${moduleType} addresses are identical:`, uniqueAddresses[0]); } // Верификация во всех сетях через отдельный скрипт - console.log(`[MODULES_DBG] Starting verification in all networks...`); + logger.info(`[MODULES_DBG] Starting verification in all networks...`); deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Начало верификации модулей во всех сетях...'); // Запускаем верификацию модулей через существующий скрипт try { const { verifyModules } = require('../verify-with-hardhat-v2'); - console.log(`[MODULES_DBG] Запускаем верификацию модулей...`); + logger.info(`[MODULES_DBG] Запускаем верификацию модулей...`); deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', 'Верификация контрактов в блокчейн-сканерах...'); await verifyModules(); - console.log(`[MODULES_DBG] Верификация модулей завершена`); + logger.info(`[MODULES_DBG] Верификация модулей завершена`); deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', 'Верификация модулей завершена успешно'); } catch (verifyError) { - console.log(`[MODULES_DBG] Ошибка при верификации модулей: ${verifyError.message}`); + logger.info(`[MODULES_DBG] Ошибка при верификации модулей: ${verifyError.message}`); deploymentWebSocketService.addDeploymentLog(dleAddress, 'error', `Ошибка при верификации модулей: ${verifyError.message}`); } @@ -562,7 +655,7 @@ async function main() { }, {}) : {} })); - console.log('MODULES_DEPLOY_RESULT', JSON.stringify(finalResults)); + logger.info('MODULES_DEPLOY_RESULT', JSON.stringify(finalResults)); // Сохраняем результаты в отдельные файлы для каждого модуля const dleDir = path.join(__dirname, '../contracts-data/modules'); @@ -602,8 +695,10 @@ async function main() { const verification = verificationResult?.modules?.[moduleType] || 'unknown'; try { - const provider = new hre.ethers.JsonRpcProvider(rpcUrl); - const network = await provider.getNetwork(); + const { provider, network } = await createRPCConnection(rpcUrl, pk, { + maxRetries: 3, + timeout: 30000 + }); moduleInfo.networks.push({ chainId: Number(network.chainId), @@ -614,7 +709,7 @@ async function main() { error: moduleResult?.error || null }); } catch (error) { - console.error(`[MODULES_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message); + logger.error(`[MODULES_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message); moduleInfo.networks.push({ chainId: null, rpcUrl: rpcUrl, @@ -630,15 +725,15 @@ async function main() { const fileName = `${moduleType}-${dleAddress.toLowerCase()}.json`; const filePath = path.join(dleDir, fileName); fs.writeFileSync(filePath, JSON.stringify(moduleInfo, null, 2)); - console.log(`[MODULES_DBG] ${moduleType} info saved to: ${filePath}`); + logger.info(`[MODULES_DBG] ${moduleType} info saved to: ${filePath}`); } - console.log('[MODULES_DBG] All modules deployment completed!'); - console.log(`[MODULES_DBG] Available modules: ${Object.keys(MODULE_CONFIGS).join(', ')}`); - console.log(`[MODULES_DBG] DLE Address: ${dleAddress}`); - console.log(`[MODULES_DBG] DLE Name: ${params.name}`); - console.log(`[MODULES_DBG] DLE Symbol: ${params.symbol}`); - console.log(`[MODULES_DBG] DLE Location: ${params.location}`); + logger.info('[MODULES_DBG] All modules deployment completed!'); + logger.info(`[MODULES_DBG] Available modules: ${Object.keys(MODULE_CONFIGS).join(', ')}`); + logger.info(`[MODULES_DBG] DLE Address: ${dleAddress}`); + logger.info(`[MODULES_DBG] DLE Name: ${params.name}`); + logger.info(`[MODULES_DBG] DLE Symbol: ${params.symbol}`); + logger.info(`[MODULES_DBG] DLE Location: ${params.location}`); // Создаем сводный отчет о деплое const summaryReport = { @@ -675,10 +770,10 @@ async function main() { // Сохраняем сводный отчет const summaryPath = path.join(__dirname, '../contracts-data/modules-deploy-summary.json'); fs.writeFileSync(summaryPath, JSON.stringify(summaryReport, null, 2)); - console.log(`[MODULES_DBG] Сводный отчет сохранен: ${summaryPath}`); + logger.info(`[MODULES_DBG] Сводный отчет сохранен: ${summaryPath}`); // Уведомляем WebSocket клиентов о завершении деплоя - console.log(`[MODULES_DBG] finalResults:`, JSON.stringify(finalResults, null, 2)); + logger.info(`[MODULES_DBG] finalResults:`, JSON.stringify(finalResults, null, 2)); const successfulModules = finalResults.reduce((acc, result) => { if (result.modules) { @@ -694,14 +789,14 @@ async function main() { const successCount = Object.keys(successfulModules).length; const totalCount = modulesToDeploy.length; - console.log(`[MODULES_DBG] successfulModules:`, successfulModules); - console.log(`[MODULES_DBG] successCount: ${successCount}, totalCount: ${totalCount}`); + logger.info(`[MODULES_DBG] successfulModules:`, successfulModules); + logger.info(`[MODULES_DBG] successCount: ${successCount}, totalCount: ${totalCount}`); if (successCount === totalCount) { - console.log(`[MODULES_DBG] Вызываем finishDeploymentSession с success=true`); + logger.info(`[MODULES_DBG] Вызываем finishDeploymentSession с success=true`); deploymentWebSocketService.finishDeploymentSession(dleAddress, true, `Деплой завершен успешно! Задеплоено ${successCount} из ${totalCount} модулей`); } else { - console.log(`[MODULES_DBG] Вызываем finishDeploymentSession с success=false`); + logger.info(`[MODULES_DBG] Вызываем finishDeploymentSession с success=false`); deploymentWebSocketService.finishDeploymentSession(dleAddress, false, `Деплой завершен с ошибками. Задеплоено ${successCount} из ${totalCount} модулей`); } @@ -709,4 +804,4 @@ async function main() { deploymentWebSocketService.notifyModulesUpdated(dleAddress); } -main().catch((e) => { console.error(e); process.exit(1); }); +main().catch((e) => { logger.error(e); process.exit(1); }); diff --git a/backend/scripts/deploy/deploy-multichain.js b/backend/scripts/deploy/deploy-multichain.js index af60427..3e50491 100755 --- a/backend/scripts/deploy/deploy-multichain.js +++ b/backend/scripts/deploy/deploy-multichain.js @@ -11,64 +11,108 @@ */ /* eslint-disable no-console */ + +// КРИТИЧЕСКИЙ ЛОГ - СКРИПТ ЗАПУЩЕН! +console.log('[MULTI_DBG] 🚀 СКРИПТ DEPLOY-MULTICHAIN.JS ЗАПУЩЕН!'); + +console.log('[MULTI_DBG] 📦 Импортируем hardhat...'); const hre = require('hardhat'); +console.log('[MULTI_DBG] ✅ hardhat импортирован'); + +console.log('[MULTI_DBG] 📦 Импортируем path...'); const path = require('path'); +console.log('[MULTI_DBG] ✅ path импортирован'); + +console.log('[MULTI_DBG] 📦 Импортируем fs...'); const fs = require('fs'); +console.log('[MULTI_DBG] ✅ fs импортирован'); + +console.log('[MULTI_DBG] 📦 Импортируем rpcProviderService...'); const { getRpcUrlByChainId } = require('../../services/rpcProviderService'); +console.log('[MULTI_DBG] ✅ rpcProviderService импортирован'); -// Подбираем безопасные gas/fee для разных сетей (включая L2) -async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { - const fee = await provider.getFeeData(); - const overrides = {}; - const minPriority = (await (async () => hre.ethers.parseUnits(minPriorityGwei.toString(), 'gwei'))()); - const minFee = (await (async () => hre.ethers.parseUnits(minFeeGwei.toString(), 'gwei'))()); - if (fee.maxFeePerGas) { - overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas; - overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n) - ? fee.maxPriorityFeePerGas - : minPriority; - } else if (fee.gasPrice) { - overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice; - } - return overrides; -} +console.log('[MULTI_DBG] 📦 Импортируем logger...'); +const logger = require('../../utils/logger'); +console.log('[MULTI_DBG] ✅ logger импортирован'); -async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit, params) { +console.log('[MULTI_DBG] 📦 Импортируем deploymentUtils...'); +const { getFeeOverrides, createProviderAndWallet, alignNonce, getNetworkInfo, createMultipleRPCConnections, sendTransactionWithRetry, createRPCConnection } = require('../../utils/deploymentUtils'); +console.log('[MULTI_DBG] ✅ deploymentUtils импортирован'); + +console.log('[MULTI_DBG] 📦 Импортируем nonceManager...'); +const { nonceManager } = require('../../utils/nonceManager'); +console.log('[MULTI_DBG] ✅ nonceManager импортирован'); + +console.log('[MULTI_DBG] 🎯 ВСЕ ИМПОРТЫ УСПЕШНЫ!'); + +console.log('[MULTI_DBG] 🔍 ПРОВЕРЯЕМ ФУНКЦИИ...'); +console.log('[MULTI_DBG] deployInNetwork:', typeof deployInNetwork); +console.log('[MULTI_DBG] main:', typeof main); + +async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit, params) { const { ethers } = hre; - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(pk, provider); - const net = await provider.getNetwork(); + + // Используем новый менеджер RPC с retry логикой + const { provider, wallet, network } = await createRPCConnection(rpcUrl, pk, { + maxRetries: 3, + timeout: 30000 + }); + + const net = network; // DEBUG: базовая информация по сети try { const calcInitHash = ethers.keccak256(dleInit); - const saltLen = ethers.getBytes(salt).length; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`); - console.log(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`); - console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`); - console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`); - console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`); - console.log(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`); + logger.info(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`); + logger.info(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`); + logger.info(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`); + logger.info(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`); } catch (e) { - console.log('[MULTI_DBG] precheck error', e?.message || e); + logger.error('[MULTI_DBG] precheck error', e?.message || e); } - // 1) Выравнивание nonce до targetDLENonce нулевыми транзакциями (если нужно) - let current = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetDLENonce}`); + // 1) Используем NonceManager для правильного управления nonce + const chainId = Number(net.chainId); + let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId); + logger.info(`[MULTI_DBG] chainId=${chainId} current nonce=${current} target=${targetDLENonce}`); if (current > targetDLENonce) { - throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`); + logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > targetDLENonce ${targetDLENonce} - waiting for sync`); + + // Ждем синхронизации nonce (максимум 60 секунд с прогрессивной задержкой) + let waitTime = 0; + let checkInterval = 1000; // Начинаем с 1 секунды + + while (current > targetDLENonce && waitTime < 60000) { + await new Promise(resolve => setTimeout(resolve, checkInterval)); + current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId); + waitTime += checkInterval; + + // Прогрессивно увеличиваем интервал проверки + if (waitTime > 10000) checkInterval = 2000; + if (waitTime > 30000) checkInterval = 5000; + + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} waiting for nonce sync: ${current} > ${targetDLENonce} (${waitTime}ms, next check in ${checkInterval}ms)`); + } + + if (current > targetDLENonce) { + const errorMsg = `Nonce sync timeout: current ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}. This may indicate network issues or the wallet was used for other transactions.`; + logger.error(`[MULTI_DBG] ${errorMsg}`); + throw new Error(errorMsg); + } + + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce sync completed: ${current} <= ${targetDLENonce}`); } if (current < targetDLENonce) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce alignment: ${current} -> ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce alignment: ${current} -> ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); } else { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned: ${current} = ${targetDLENonce}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned: ${current} = ${targetDLENonce}`); } if (current < targetDLENonce) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); // Используем burn address для более надежных транзакций const burnAddress = "0x000000000000000000000000000000000000dEaD"; @@ -79,7 +123,7 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d let sent = false; let lastErr = null; - for (let attempt = 0; attempt < 3 && !sent; attempt++) { + for (let attempt = 0; attempt < 5 && !sent; attempt++) { try { const txReq = { to: burnAddress, // отправляем на burn address вместо своего адреса @@ -88,49 +132,87 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d gasLimit, ...overrides }; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`); - const txFill = await wallet.sendTransaction(txReq); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); - await txFill.wait(); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}/5`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`); + const { tx: txFill, receipt } = await sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 }); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); sent = true; } catch (e) { lastErr = e; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`); + const errorMsg = e?.message || e; + logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${errorMsg}`); - if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) { - gasLimit = 50000; // увеличиваем gas limit + // Обработка специфических ошибок + if (String(errorMsg).toLowerCase().includes('intrinsic gas too low') && attempt < 4) { + gasLimit = Math.min(gasLimit * 2, 100000); // увеличиваем gas limit с ограничением + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} increased gas limit to ${gasLimit}`); continue; } - if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) { - // Обновляем nonce и пробуем снова - current = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`); + if (String(errorMsg).toLowerCase().includes('nonce too low') && attempt < 4) { + // Сбрасываем кэш nonce и получаем актуальный + nonceManager.resetNonce(wallet.address, chainId); + const newNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce changed from ${current} to ${newNonce}`); + current = newNonce; + + // Если новый nonce больше целевого, обновляем targetDLENonce + if (current > targetDLENonce) { + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > target nonce ${targetDLENonce}, updating target`); + targetDLENonce = current; + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`); + } + continue; } - throw e; + if (String(errorMsg).toLowerCase().includes('insufficient funds') && attempt < 4) { + logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} insufficient funds for nonce alignment`); + throw new Error(`Insufficient funds for nonce alignment on chainId=${Number(net.chainId)}. Please add more ETH to the wallet.`); + } + + if (String(errorMsg).toLowerCase().includes('network') && attempt < 4) { + logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} network error, retrying in ${(attempt + 1) * 2} seconds...`); + await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 2000)); + continue; + } + + // Если это последняя попытка, выбрасываем ошибку + if (attempt === 4) { + throw new Error(`Failed to send filler transaction after 5 attempts: ${errorMsg}`); + } } } if (!sent) { - console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); + logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); throw lastErr || new Error('filler tx failed'); } current++; } - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); + + // Зарезервируем nonce в NonceManager + nonceManager.reserveNonce(wallet.address, chainId, targetDLENonce); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`); } else { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); } - // 2) Деплой DLE напрямую на согласованном nonce - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE directly with nonce=${targetDLENonce}`); + // 2) Проверяем баланс перед деплоем + const balance = await provider.getBalance(wallet.address, 'latest'); + const balanceEth = ethers.formatEther(balance); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} wallet balance: ${balanceEth} ETH`); + + if (balance < ethers.parseEther('0.01')) { + throw new Error(`Insufficient balance for deployment on chainId=${Number(net.chainId)}. Current: ${balanceEth} ETH, required: 0.01 ETH minimum`); + } + + // 3) Деплой DLE с актуальным nonce + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE with current nonce`); const feeOverrides = await getFeeOverrides(provider); let gasLimit; @@ -147,90 +229,137 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance); gasLimit = est ? (est + est / 5n) : fallbackGas; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`); } catch (_) { gasLimit = 3_000_000n; } - // Вычисляем предсказанный адрес DLE - const predictedAddress = ethers.getCreateAddress({ + // Вычисляем предсказанный адрес DLE с целевым nonce (детерминированный деплой) + let predictedAddress = ethers.getCreateAddress({ from: wallet.address, nonce: targetDLENonce }); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress} (nonce=${targetDLENonce})`); // Проверяем, не развернут ли уже контракт const existingCode = await provider.getCode(predictedAddress); if (existingCode && existingCode !== '0x') { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`); // Проверяем и инициализируем логотип для существующего контракта if (params.logoURI && params.logoURI !== '') { try { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} checking logoURI for existing contract`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} checking logoURI for existing contract`); const DLE = await hre.ethers.getContractFactory('DLE'); const dleContract = DLE.attach(predictedAddress); const currentLogo = await dleContract.logoURI(); if (currentLogo === '' || currentLogo === '0x') { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI for existing contract: ${params.logoURI}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI for existing contract: ${params.logoURI}`); const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides); await logoTx.wait(); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized for existing contract`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized for existing contract`); } else { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI already set: ${currentLogo}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI already set: ${currentLogo}`); } } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed for existing contract: ${error.message}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed for existing contract: ${error.message}`); } } return { address: predictedAddress, chainId: Number(net.chainId) }; } - // Деплоим DLE + // Деплоим DLE с retry логикой для обработки race conditions let tx; - try { - tx = await wallet.sendTransaction({ - data: dleInit, - nonce: targetDLENonce, - gasLimit, - ...feeOverrides - }); - } catch (e) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`); - // Повторная попытка с обновленным nonce - const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`); - tx = await wallet.sendTransaction({ - data: dleInit, - nonce: updatedNonce, - gasLimit, - ...feeOverrides - }); + let deployAttempts = 0; + const maxDeployAttempts = 5; + + while (deployAttempts < maxDeployAttempts) { + try { + deployAttempts++; + + // Получаем актуальный nonce прямо перед отправкой транзакции + const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts}/${maxDeployAttempts} with current nonce=${currentNonce} (target was ${targetDLENonce})`); + + const txData = { + data: dleInit, + nonce: currentNonce, + gasLimit, + ...feeOverrides + }; + + const result = await sendTransactionWithRetry(wallet, txData, { maxRetries: 3 }); + tx = result.tx; + + // Отмечаем транзакцию как pending в NonceManager + nonceManager.markTransactionPending(wallet.address, chainId, currentNonce, tx.hash); + + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy successful on attempt ${deployAttempts}`); + break; // Успешно отправили, выходим из цикла + + } catch (e) { + const errorMsg = e?.message || e; + logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts} failed: ${errorMsg}`); + + // Проверяем, является ли это ошибкой nonce + if (String(errorMsg).toLowerCase().includes('nonce too low') && deployAttempts < maxDeployAttempts) { + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce race condition detected, retrying...`); + + // Получаем актуальный nonce из сети + const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce: ${currentNonce}, target was: ${targetDLENonce}`); + + // Обновляем targetDLENonce на актуальный nonce + targetDLENonce = currentNonce; + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`); + + // Короткая задержка перед следующей попыткой + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + + // Если это не ошибка nonce или исчерпаны попытки, выбрасываем ошибку + if (deployAttempts >= maxDeployAttempts) { + throw new Error(`Deployment failed after ${maxDeployAttempts} attempts: ${errorMsg}`); + } + + // Для других ошибок делаем короткую задержку и пробуем снова + await new Promise(resolve => setTimeout(resolve, 2000)); + } } const rc = await tx.wait(); + + // Отмечаем транзакцию как подтвержденную в NonceManager + nonceManager.markTransactionConfirmed(wallet.address, chainId, tx.hash); const deployedAddress = rc.contractAddress || predictedAddress; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`); + // Проверяем, что адрес соответствует предсказанному + if (deployedAddress !== predictedAddress) { + logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} ADDRESS MISMATCH! predicted=${predictedAddress} actual=${deployedAddress}`); + throw new Error(`Address mismatch: predicted ${predictedAddress} != actual ${deployedAddress}`); + } + + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress} ✅`); // Инициализация логотипа если он указан if (params.logoURI && params.logoURI !== '') { try { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI: ${params.logoURI}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI: ${params.logoURI}`); const DLE = await hre.ethers.getContractFactory('DLE'); const dleContract = DLE.attach(deployedAddress); const logoTx = await dleContract.connect(wallet).initializeLogoURI(params.logoURI, feeOverrides); await logoTx.wait(); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized successfully`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized successfully`); } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${error.message}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${error.message}`); // Не прерываем деплой из-за ошибки логотипа } } else { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} no logoURI specified, skipping initialization`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} no logoURI specified, skipping initialization`); } return { address: deployedAddress, chainId: Number(net.chainId) }; @@ -238,9 +367,34 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d async function main() { + console.log('[MULTI_DBG] 🚀 ВХОДИМ В ФУНКЦИЮ MAIN!'); const { ethers } = hre; + console.log('[MULTI_DBG] ✅ ethers получен'); + + logger.info('[MULTI_DBG] 🚀 НАЧИНАЕМ ДЕПЛОЙ DLE КОНТРАКТА'); + console.log('[MULTI_DBG] ✅ logger.info выполнен'); + + // Автоматически генерируем ABI и flattened контракт перед деплоем + logger.info('🔨 Генерация ABI файла...'); + try { + const { generateABIFile } = require('../generate-abi'); + generateABIFile(); + logger.info('✅ ABI файл обновлен перед деплоем'); + } catch (abiError) { + logger.warn('⚠️ Ошибка генерации ABI:', abiError.message); + } + + logger.info('🔨 Генерация flattened контракта...'); + try { + const { generateFlattened } = require('../generate-flattened'); + await generateFlattened(); + logger.info('✅ Flattened контракт обновлен перед деплоем'); + } catch (flattenError) { + logger.warn('⚠️ Ошибка генерации flattened контракта:', flattenError.message); + } // Загружаем параметры из базы данных или файла + console.log('[MULTI_DBG] 🔍 НАЧИНАЕМ ЗАГРУЗКУ ПАРАМЕТРОВ...'); let params; try { @@ -251,10 +405,10 @@ async function main() { // Проверяем, передан ли конкретный deploymentId const deploymentId = process.env.DEPLOYMENT_ID; if (deploymentId) { - console.log(`🔍 Ищем параметры для deploymentId: ${deploymentId}`); + logger.info(`🔍 Ищем параметры для deploymentId: ${deploymentId}`); params = await deployParamsService.getDeployParams(deploymentId); if (params) { - console.log('✅ Параметры загружены из базы данных по deploymentId'); + logger.info('✅ Параметры загружены из базы данных по deploymentId'); } else { throw new Error(`Параметры деплоя не найдены для deploymentId: ${deploymentId}`); } @@ -263,7 +417,7 @@ async function main() { const latestParams = await deployParamsService.getLatestDeployParams(1); if (latestParams.length > 0) { params = latestParams[0]; - console.log('✅ Параметры загружены из базы данных (последние)'); + logger.info('✅ Параметры загружены из базы данных (последние)'); } else { throw new Error('Параметры деплоя не найдены в базе данных'); } @@ -271,179 +425,197 @@ async function main() { await deployParamsService.close(); } catch (dbError) { - console.log('⚠️ Не удалось загрузить параметры из БД, пытаемся загрузить из файла:', dbError.message); - - // Fallback к файлу - const paramsPath = path.join(__dirname, './current-params.json'); - if (!fs.existsSync(paramsPath)) { - throw new Error('Файл параметров не найден: ' + paramsPath); + logger.error('❌ Критическая ошибка: не удалось загрузить параметры из БД:', dbError.message); + throw new Error(`Деплой невозможен без параметров из БД: ${dbError.message}`); } - - params = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); - console.log('✅ Параметры загружены из файла'); - } - console.log('[MULTI_DBG] Загружены параметры:', { + logger.info('[MULTI_DBG] Загружены параметры:', { name: params.name, symbol: params.symbol, supportedChainIds: params.supportedChainIds, - CREATE2_SALT: params.CREATE2_SALT + rpcUrls: params.rpcUrls || params.rpc_urls, + etherscanApiKey: params.etherscanApiKey || params.etherscan_api_key }); const pk = params.private_key || process.env.PRIVATE_KEY; - const salt = params.CREATE2_SALT || params.create2_salt; const networks = params.rpcUrls || params.rpc_urls || []; + // Устанавливаем API ключи Etherscan для верификации + const ApiKeyManager = require('../../utils/apiKeyManager'); + const etherscanKey = ApiKeyManager.getAndSetEtherscanApiKey(params); + + if (!etherscanKey) { + logger.warn('[MULTI_DBG] ⚠️ Etherscan API ключ не найден - верификация будет пропущена'); + logger.warn(`[MULTI_DBG] Доступные поля: ${Object.keys(params).join(', ')}`); + } + if (!pk) throw new Error('Env: PRIVATE_KEY'); - if (!salt) throw new Error('CREATE2_SALT not found in params'); if (networks.length === 0) throw new Error('RPC URLs not found in params'); // Prepare init code once const DLE = await hre.ethers.getContractFactory('contracts/DLE.sol:DLE'); - const dleConfig = { - name: params.name || '', - symbol: params.symbol || '', - location: params.location || '', - coordinates: params.coordinates || '', - jurisdiction: params.jurisdiction || 0, - oktmo: params.oktmo || '', - okvedCodes: params.okvedCodes || [], - kpp: params.kpp ? BigInt(params.kpp) : 0n, - quorumPercentage: params.quorumPercentage || 51, - initialPartners: params.initialPartners || [], - initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount) * BigInt(10**18)), - supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id)) - }; - const deployTx = await DLE.getDeployTransaction(dleConfig, BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000"); - const dleInit = deployTx.data; - const initCodeHash = ethers.keccak256(dleInit); + + // Используем централизованный генератор параметров конструктора + const { generateDeploymentArgs } = require('../../utils/constructorArgsGenerator'); + const { dleConfig, initializer } = generateDeploymentArgs(params); + // Проверяем наличие поддерживаемых сетей + const supportedChainIds = params.supportedChainIds || []; + if (supportedChainIds.length === 0) { + throw new Error('Не указаны поддерживаемые сети (supportedChainIds)'); + } + + // Создаем initCode для каждой сети отдельно + const initCodes = {}; + for (const chainId of supportedChainIds) { + const deployTx = await DLE.getDeployTransaction(dleConfig, initializer); + initCodes[chainId] = deployTx.data; + } + + // Получаем initCodeHash из первого initCode (все должны быть одинаковые по структуре) + const firstChainId = supportedChainIds[0]; + const firstInitCode = initCodes[firstChainId]; + if (!firstInitCode) { + throw new Error(`InitCode не создан для первой сети: ${firstChainId}`); + } + const initCodeHash = ethers.keccak256(firstInitCode); // DEBUG: глобальные значения try { - const saltLen = ethers.getBytes(salt).length; - console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`); - console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`); - console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`); + logger.info(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`); + logger.info(`[MULTI_DBG] GLOBAL firstInitCode.lenBytes=${ethers.getBytes(firstInitCode).length} head16=${firstInitCode.slice(0, 34)}...`); } catch (e) { - console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e); + logger.info('[MULTI_DBG] GLOBAL precheck error', e?.message || e); } - // Подготовим провайдеры и вычислим общий nonce для DLE - const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u)); - const wallets = providers.map(p => new hre.ethers.Wallet(pk, p)); + // Подготовим провайдеры и вычислим общий nonce для DLE с retry логикой + logger.info(`[MULTI_DBG] Создаем RPC соединения для ${networks.length} сетей...`); + const connections = await createMultipleRPCConnections(networks, pk, { + maxRetries: 3, + timeout: 30000 + }); + + if (connections.length === 0) { + throw new Error('Не удалось установить ни одного RPC соединения'); + } + + logger.info(`[MULTI_DBG] ✅ Успешно подключились к ${connections.length}/${networks.length} сетям`); + + // Очищаем старые pending транзакции для всех сетей + for (const connection of connections) { + const chainId = Number(connection.network.chainId); + nonceManager.clearOldPendingTransactions(connection.wallet.address, chainId); + } + const nonces = []; - for (let i = 0; i < providers.length; i++) { - const n = await providers[i].getTransactionCount(wallets[i].address, 'pending'); + for (const connection of connections) { + const n = await nonceManager.getNonce(connection.wallet.address, connection.rpcUrl, Number(connection.network.chainId)); nonces.push(n); } const targetDLENonce = Math.max(...nonces); - console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`); - console.log(`[MULTI_DBG] Starting deployment to ${networks.length} networks:`, networks); + logger.info(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`); + logger.info(`[MULTI_DBG] Starting deployment to ${networks.length} networks:`, networks); - // ПАРАЛЛЕЛЬНЫЙ деплой во всех сетях одновременно - console.log(`[MULTI_DBG] Starting PARALLEL deployment to ${networks.length} networks`); + // ПАРАЛЛЕЛЬНЫЙ деплой во всех успешных сетях одновременно + console.log(`[MULTI_DBG] 🚀 ДОШЛИ ДО ПАРАЛЛЕЛЬНОГО ДЕПЛОЯ!`); + logger.info(`[MULTI_DBG] Starting PARALLEL deployment to ${connections.length} successful networks`); + logger.info(`[MULTI_DBG] 🚀 ЗАПУСКАЕМ ЦИКЛ ДЕПЛОЯ!`); - const deploymentPromises = networks.map(async (rpcUrl, i) => { - console.log(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${networks.length}: ${rpcUrl}`); + const deploymentPromises = connections.map(async (connection, i) => { + const rpcUrl = connection.rpcUrl; + const chainId = Number(connection.network.chainId); + + logger.info(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${connections.length}: ${rpcUrl} (chainId: ${chainId})`); try { - // Получаем chainId динамически из сети - const provider = new hre.ethers.JsonRpcProvider(rpcUrl); - const network = await provider.getNetwork(); - const chainId = Number(network.chainId); + // Получаем правильный initCode для этой сети + const networkInitCode = initCodes[chainId]; + if (!networkInitCode) { + throw new Error(`InitCode не найден для chainId: ${chainId}`); + } - console.log(`[MULTI_DBG] 📡 Network ${i + 1} chainId: ${chainId}`); - - const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit, params); - console.log(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`); - return { rpcUrl, chainId, ...r }; + const r = await deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, networkInitCode, params); + logger.info(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`); + return { rpcUrl, chainId, address: r.address, chainId: r.chainId }; } catch (error) { - console.error(`[MULTI_DBG] ❌ Network ${i + 1} deployment FAILED:`, error.message); - return { rpcUrl, error: error.message }; + logger.error(`[MULTI_DBG] ❌ Network ${i + 1} deployment FAILED:`, error.message); + return { rpcUrl, chainId, error: error.message }; } }); // Ждем завершения всех деплоев const results = await Promise.all(deploymentPromises); - console.log(`[MULTI_DBG] All ${networks.length} deployments completed`); + logger.info(`[MULTI_DBG] All ${networks.length} deployments completed`); // Логируем результаты для каждой сети results.forEach((result, index) => { if (result.address) { - console.log(`[MULTI_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS: ${result.address}`); + logger.info(`[MULTI_DBG] ✅ Network ${index + 1} (chainId: ${result.chainId}) SUCCESS: ${result.address}`); } else { - console.log(`[MULTI_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`); + logger.info(`[MULTI_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`); } }); - // Проверяем, что все адреса одинаковые + // Проверяем, что все адреса одинаковые (критично для детерминированного деплоя) const addresses = results.map(r => r.address).filter(addr => addr); const uniqueAddresses = [...new Set(addresses)]; - console.log('[MULTI_DBG] All addresses:', addresses); - console.log('[MULTI_DBG] Unique addresses:', uniqueAddresses); - console.log('[MULTI_DBG] Results count:', results.length); - console.log('[MULTI_DBG] Networks count:', networks.length); + logger.info('[MULTI_DBG] All addresses:', addresses); + logger.info('[MULTI_DBG] Unique addresses:', uniqueAddresses); + logger.info('[MULTI_DBG] Results count:', results.length); + logger.info('[MULTI_DBG] Networks count:', networks.length); if (uniqueAddresses.length > 1) { - console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!'); - console.error('[MULTI_DBG] addresses:', uniqueAddresses); + logger.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!'); + logger.error('[MULTI_DBG] addresses:', uniqueAddresses); throw new Error('Nonce alignment failed - addresses are different'); } if (uniqueAddresses.length === 0) { - console.error('[MULTI_DBG] ERROR: No successful deployments!'); + logger.error('[MULTI_DBG] ERROR: No successful deployments!'); throw new Error('No successful deployments'); } - console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]); + logger.info('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]); - // Автоматическая верификация контрактов - let verificationResults = []; - - console.log(`[MULTI_DBG] autoVerifyAfterDeploy: ${params.autoVerifyAfterDeploy}`); - - if (params.autoVerifyAfterDeploy) { - console.log('[MULTI_DBG] Starting automatic contract verification...'); - - try { - // Импортируем функцию верификации - const { verifyWithHardhatV2 } = require('../verify-with-hardhat-v2'); - - // Подготавливаем данные о развернутых сетях - const deployedNetworks = results - .filter(result => result.address && !result.error) - .map(result => ({ - chainId: result.chainId, - address: result.address - })); - - // Запускаем верификацию с данными о сетях - await verifyWithHardhatV2(params, deployedNetworks); - - // Если верификация прошла успешно, отмечаем все как верифицированные - verificationResults = networks.map(() => 'verified'); - console.log('[MULTI_DBG] ✅ Automatic verification completed successfully'); - - } catch (verificationError) { - console.error('[MULTI_DBG] ❌ Automatic verification failed:', verificationError.message); - verificationResults = networks.map(() => 'verification_failed'); - } - } else { - console.log('[MULTI_DBG] Contract verification disabled (autoVerifyAfterDeploy: false)'); - verificationResults = networks.map(() => 'disabled'); - } - - // Объединяем результаты + // ВЫВОДИМ РЕЗУЛЬТАТ СРАЗУ ПОСЛЕ ДЕПЛОЯ (ПЕРЕД ВЕРИФИКАЦИЕЙ)! + console.log('[MULTI_DBG] 🎯 ДОШЛИ ДО ВЫВОДА РЕЗУЛЬТАТА!'); const finalResults = results.map((result, index) => ({ ...result, - verification: verificationResults[index] || 'failed' + verification: 'pending' })); + console.log('[MULTI_DBG] 📊 finalResults:', JSON.stringify(finalResults, null, 2)); + console.log('[MULTI_DBG] 🎯 ВЫВОДИМ MULTICHAIN_DEPLOY_RESULT!'); console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults)); + console.log('[MULTI_DBG] ✅ MULTICHAIN_DEPLOY_RESULT ВЫВЕДЕН!'); + logger.info('[MULTI_DBG] DLE deployment completed successfully!'); - console.log('[MULTI_DBG] DLE deployment completed successfully!'); + // Верификация контрактов отключена + logger.info('[MULTI_DBG] Contract verification disabled - skipping verification step'); + + // Отмечаем все результаты как без верификации + const finalResultsWithVerification = results.map((result) => ({ + ...result, + verification: 'skipped' + })); + + logger.info('[MULTI_DBG] Verification skipped - deployment completed successfully'); } -main().catch((e) => { console.error(e); process.exit(1); }); +console.log('[MULTI_DBG] 🚀 ВЫЗЫВАЕМ MAIN()...'); +main().catch((e) => { + console.log('[MULTI_DBG] ❌ ОШИБКА В MAIN:', e); + logger.error('[MULTI_DBG] ❌ Deployment failed:', e); + + // Даже при ошибке выводим результат для анализа + const errorResult = { + error: e.message, + success: false, + timestamp: new Date().toISOString(), + stack: e.stack + }; + + console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify([errorResult])); + process.exit(1); +}); diff --git a/backend/scripts/generate-abi.js b/backend/scripts/generate-abi.js new file mode 100644 index 0000000..8bc9d9f --- /dev/null +++ b/backend/scripts/generate-abi.js @@ -0,0 +1,119 @@ +/** + * Автоматическая генерация ABI для фронтенда + * Извлекает ABI из скомпилированных артефактов Hardhat + */ + +const fs = require('fs'); +const path = require('path'); + +// Пути к артефактам +const artifactsPath = path.join(__dirname, '../artifacts/contracts'); +const frontendAbiPath = path.join(__dirname, '../../frontend/src/utils/dle-abi.js'); + +// Создаем директорию если она не существует +const frontendDir = path.dirname(frontendAbiPath); +if (!fs.existsSync(frontendDir)) { + fs.mkdirSync(frontendDir, { recursive: true }); + console.log('✅ Создана директория:', frontendDir); +} + +// Функция для извлечения ABI из артефакта +function extractABI(contractName) { + const artifactPath = path.join(artifactsPath, `${contractName}.sol`, `${contractName}.json`); + + if (!fs.existsSync(artifactPath)) { + console.log(`⚠️ Артефакт не найден: ${artifactPath}`); + return null; + } + + try { + const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8')); + return artifact.abi; + } catch (error) { + console.error(`❌ Ошибка чтения артефакта ${contractName}:`, error.message); + return null; + } +} + +// Функция для форматирования ABI в строку +function formatABI(abi) { + const functions = abi.filter(item => item.type === 'function'); + const events = abi.filter(item => item.type === 'event'); + + let result = 'export const DLE_ABI = [\n'; + + // Функции + functions.forEach(func => { + const inputs = func.inputs.map(input => `${input.type} ${input.name}`).join(', '); + const outputs = func.outputs.map(output => output.type).join(', '); + const returns = outputs ? ` returns (${outputs})` : ''; + + result += ` "${func.type} ${func.name}(${inputs})${returns}",\n`; + }); + + // События + events.forEach(event => { + const inputs = event.inputs.map(input => `${input.type} ${input.name}`).join(', '); + result += ` "event ${event.name}(${inputs})",\n`; + }); + + result += '];\n'; + return result; +} + +// Функция для генерации полного файла ABI +function generateABIFile() { + console.log('🔨 Генерация ABI файла...'); + + // Извлекаем ABI для DLE контракта + const dleABI = extractABI('DLE'); + + if (!dleABI) { + console.error('❌ Не удалось извлечь ABI для DLE контракта'); + return; + } + + // Форматируем ABI + const formattedABI = formatABI(dleABI); + + // Создаем полный файл + const fileContent = `/** + * ABI для DLE смарт-контракта + * АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ + * Для обновления запустите: node backend/scripts/generate-abi.js + * + * Последнее обновление: ${new Date().toISOString()} + */ + +${formattedABI} + +// ABI для деактивации (специальные функции) - НЕ СУЩЕСТВУЮТ В КОНТРАКТЕ +export const DLE_DEACTIVATION_ABI = [ + // Эти функции не существуют в контракте DLE +]; + +// ABI для токенов (базовые функции) +export const TOKEN_ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function decimals() view returns (uint8)", + "function totalSupply() view returns (uint256)" +]; +`; + + // Записываем файл + try { + fs.writeFileSync(frontendAbiPath, fileContent, 'utf8'); + console.log('✅ ABI файл успешно сгенерирован:', frontendAbiPath); + console.log(`📊 Функций: ${dleABI.filter(item => item.type === 'function').length}`); + console.log(`📊 Событий: ${dleABI.filter(item => item.type === 'event').length}`); + } catch (error) { + console.error('❌ Ошибка записи ABI файла:', error.message); + } +} + +// Запуск генерации +if (require.main === module) { + generateABIFile(); +} + +module.exports = { generateABIFile, extractABI, formatABI }; diff --git a/backend/scripts/generate-flattened.js b/backend/scripts/generate-flattened.js new file mode 100644 index 0000000..4443735 --- /dev/null +++ b/backend/scripts/generate-flattened.js @@ -0,0 +1,93 @@ +/** + * Автоматическая генерация flattened контракта для верификации + * Создает DLE_flattened.sol из DLE.sol с помощью hardhat flatten + */ + +const { spawn, execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +// Пути к файлам +const contractsDir = path.join(__dirname, '../contracts'); +const dleContractPath = path.join(contractsDir, 'DLE.sol'); +const flattenedPath = path.join(contractsDir, 'DLE_flattened.sol'); + +// Функция для генерации flattened контракта +function generateFlattened() { + return new Promise((resolve, reject) => { + console.log('🔨 Генерация flattened контракта...'); + + // Проверяем существование исходного контракта + if (!fs.existsSync(dleContractPath)) { + reject(new Error(`Исходный контракт не найден: ${dleContractPath}`)); + return; + } + + // Запускаем hardhat flatten с перенаправлением в файл + try { + console.log('🔨 Выполняем hardhat flatten...'); + execSync(`npx hardhat flatten contracts/DLE.sol > "${flattenedPath}"`, { + cwd: path.join(__dirname, '..'), + shell: true + }); + + // Проверяем, что файл создался + if (fs.existsSync(flattenedPath)) { + const stats = fs.statSync(flattenedPath); + console.log('✅ Flattened контракт создан:', flattenedPath); + console.log(`📊 Размер файла: ${(stats.size / 1024).toFixed(2)} KB`); + resolve(); + } else { + reject(new Error('Файл не был создан')); + } + } catch (error) { + console.error('❌ Ошибка выполнения hardhat flatten:', error.message); + reject(new Error(`Ошибка flatten: ${error.message}`)); + } + }); +} + +// Функция для проверки актуальности +function checkFlattenedFreshness() { + if (!fs.existsSync(flattenedPath)) { + return false; + } + + if (!fs.existsSync(dleContractPath)) { + return false; + } + + const flattenedStats = fs.statSync(flattenedPath); + const contractStats = fs.statSync(dleContractPath); + + // Flattened файл старше контракта + return flattenedStats.mtime >= contractStats.mtime; +} + +// Основная функция +async function main() { + try { + console.log('🔍 Проверка актуальности flattened контракта...'); + + const isFresh = checkFlattenedFreshness(); + + if (isFresh) { + console.log('✅ Flattened контракт актуален'); + return; + } + + console.log('🔄 Flattened контракт устарел, генерируем новый...'); + await generateFlattened(); + + } catch (error) { + console.error('❌ Ошибка генерации flattened контракта:', error.message); + process.exit(1); + } +} + +// Запуск если вызван напрямую +if (require.main === module) { + main(); +} + +module.exports = { generateFlattened, checkFlattenedFreshness }; diff --git a/backend/scripts/run-all-tests.js b/backend/scripts/run-all-tests.js deleted file mode 100644 index 0745f31..0000000 --- a/backend/scripts/run-all-tests.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Главный скрипт для запуска всех тестов - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - */ - -const { spawn } = require('child_process'); -const path = require('path'); - -console.log('🧪 ЗАПУСК ВСЕХ ТЕСТОВ ДЛЯ ВЫЯВЛЕНИЯ ПРОБЛЕМЫ'); -console.log('=' .repeat(70)); - -const tests = [ - { - name: 'Тест создания файла', - script: './test-file-creation.js', - description: 'Проверяет базовое создание и обновление файла current-params.json' - }, - { - name: 'Тест полного потока деплоя', - script: './test-deploy-flow.js', - description: 'Имитирует полный процесс деплоя DLE с созданием файла' - }, - { - name: 'Тест сохранения файла', - script: './test-file-persistence.js', - description: 'Проверяет сохранение файла после различных операций' - }, - { - name: 'Тест обработки ошибок', - script: './test-error-handling.js', - description: 'Проверяет поведение при ошибках деплоя' - } -]; - -async function runTest(testInfo, index) { - return new Promise((resolve, reject) => { - console.log(`\n${index + 1}️⃣ ${testInfo.name}`); - console.log(`📝 ${testInfo.description}`); - console.log('─'.repeat(50)); - - const testPath = path.join(__dirname, testInfo.script); - const testProcess = spawn('node', [testPath], { - stdio: 'inherit', - cwd: __dirname - }); - - testProcess.on('close', (code) => { - if (code === 0) { - console.log(`✅ ${testInfo.name} - УСПЕШНО`); - resolve(true); - } else { - console.log(`❌ ${testInfo.name} - ОШИБКА (код: ${code})`); - resolve(false); - } - }); - - testProcess.on('error', (error) => { - console.log(`❌ ${testInfo.name} - ОШИБКА ЗАПУСКА: ${error.message}`); - resolve(false); - }); - }); -} - -async function runAllTests() { - console.log('🚀 Запуск всех тестов...\n'); - - const results = []; - - for (let i = 0; i < tests.length; i++) { - const result = await runTest(tests[i], i); - results.push({ - name: tests[i].name, - success: result - }); - - // Небольшая пауза между тестами - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - // Итоговый отчет - console.log('\n' + '='.repeat(70)); - console.log('📊 ИТОГОВЫЙ ОТЧЕТ ТЕСТОВ'); - console.log('='.repeat(70)); - - const successfulTests = results.filter(r => r.success).length; - const totalTests = results.length; - - results.forEach((result, index) => { - const status = result.success ? '✅' : '❌'; - console.log(`${index + 1}. ${status} ${result.name}`); - }); - - console.log(`\n📈 Результаты: ${successfulTests}/${totalTests} тестов прошли успешно`); - - if (successfulTests === totalTests) { - console.log('🎉 ВСЕ ТЕСТЫ ПРОШЛИ УСПЕШНО!'); - console.log('💡 Проблема НЕ в базовых операциях с файлами'); - console.log('🔍 Возможные причины проблемы:'); - console.log(' - Процесс деплоя прерывается до создания файла'); - console.log(' - Ошибка в логике dleV2Service.js'); - console.log(' - Проблема с правами доступа к файлам'); - console.log(' - Конфликт с другими процессами'); - } else { - console.log('⚠️ НЕКОТОРЫЕ ТЕСТЫ НЕ ПРОШЛИ'); - console.log('🔍 Это поможет локализовать проблему'); - } - - console.log('\n🛠️ СЛЕДУЮЩИЕ ШАГИ:'); - console.log('1. Запустите: node debug-file-monitor.js (в отдельном терминале)'); - console.log('2. Запустите деплой DLE в другом терминале'); - console.log('3. Наблюдайте за созданием/удалением файлов в реальном времени'); - console.log('4. Проверьте логи Docker: docker logs dapp-backend -f'); - - console.log('\n📋 ДОПОЛНИТЕЛЬНЫЕ КОМАНДЫ ДЛЯ ОТЛАДКИ:'); - console.log('# Проверить права доступа к директориям:'); - console.log('ls -la backend/scripts/deploy/'); - console.log('ls -la backend/temp/'); - console.log(''); - console.log('# Проверить процессы Node.js:'); - console.log('ps aux | grep node'); - console.log(''); - console.log('# Проверить использование диска:'); - console.log('df -h backend/scripts/deploy/'); -} - -// Запускаем все тесты -runAllTests().catch(error => { - console.error('❌ КРИТИЧЕСКАЯ ОШИБКА:', error.message); - process.exit(1); -}); diff --git a/backend/scripts/test-ai-queue-docker.js b/backend/scripts/test-ai-queue-docker.js deleted file mode 100644 index 76a443c..0000000 --- a/backend/scripts/test-ai-queue-docker.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - * All rights reserved. - * - * This software is proprietary and confidential. - * Unauthorized copying, modification, or distribution is prohibited. - * - * For licensing inquiries: info@hb3-accelerator.com - * Website: https://hb3-accelerator.com - * GitHub: https://github.com/HB3-ACCELERATOR - */ - -// Устанавливаем переменные окружения для Docker -process.env.OLLAMA_BASE_URL = 'http://ollama:11434'; -process.env.OLLAMA_MODEL = 'qwen2.5:7b'; - -const aiQueueService = require('../services/ai-queue'); - -async function testQueueInDocker() { - // console.log('🐳 Тестирование AI очереди в Docker...\n'); - - try { - // Проверяем инициализацию - // console.log('1. Проверка инициализации очереди...'); - const stats = aiQueueService.getStats(); - // console.log('✅ Очередь инициализирована:', stats.isInitialized); - // console.log('📊 Статистика:', { - totalProcessed: stats.totalProcessed, - totalFailed: stats.totalFailed, - currentQueueSize: stats.currentQueueSize, - runningTasks: stats.runningTasks - }); - - // Тестируем добавление задач - console.log('\n2. Тестирование добавления задач...'); - - const testTasks = [ - { - message: 'Привет, как дела?', - language: 'ru', - type: 'chat', - userId: 1, - userRole: 'user', - requestId: 'docker_test_1' - }, - { - message: 'Расскажи о погоде', - language: 'ru', - type: 'analysis', - userId: 1, - userRole: 'user', - requestId: 'docker_test_2' - }, - { - message: 'Срочный вопрос!', - language: 'ru', - type: 'urgent', - userId: 1, - userRole: 'admin', - requestId: 'docker_test_3' - } - ]; - - for (let i = 0; i < testTasks.length; i++) { - const task = testTasks[i]; - console.log(` Добавляем задачу ${i + 1}: "${task.message}"`); - - try { - const result = await aiQueueService.addTask(task); - console.log(` ✅ Задача добавлена, ID: ${result.taskId}`); - } catch (error) { - console.log(` ❌ Ошибка добавления задачи: ${error.message}`); - } - } - - // Ждем обработки - console.log('\n3. Ожидание обработки задач...'); - await new Promise(resolve => setTimeout(resolve, 15000)); - - // Проверяем статистику - console.log('\n4. Проверка статистики после обработки...'); - const finalStats = aiQueueService.getStats(); - console.log('📊 Финальная статистика:', { - totalProcessed: finalStats.totalProcessed, - totalFailed: finalStats.totalFailed, - currentQueueSize: finalStats.currentQueueSize, - runningTasks: finalStats.runningTasks, - averageProcessingTime: Math.round(finalStats.averageProcessingTime) - }); - - // Тестируем управление очередью - console.log('\n5. Тестирование управления очередью...'); - - console.log(' Пауза очереди...'); - aiQueueService.pause(); - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log(' Возобновление очереди...'); - aiQueueService.resume(); - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log('\n✅ Тестирование завершено!'); - - } catch (error) { - console.error('❌ Ошибка тестирования:', error); - } -} - -// Запуск теста -if (require.main === module) { - testQueueInDocker().then(() => { - console.log('\n🏁 Тест завершен'); - process.exit(0); - }).catch(error => { - console.error('💥 Критическая ошибка:', error); - process.exit(1); - }); -} - -module.exports = { testQueueInDocker }; \ No newline at end of file diff --git a/backend/scripts/test-ai-queue.js b/backend/scripts/test-ai-queue.js deleted file mode 100644 index b5e60b3..0000000 --- a/backend/scripts/test-ai-queue.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - * All rights reserved. - * - * This software is proprietary and confidential. - * Unauthorized copying, modification, or distribution is prohibited. - * - * For licensing inquiries: info@hb3-accelerator.com - * Website: https://hb3-accelerator.com - * GitHub: https://github.com/HB3-ACCELERATOR - */ - -const aiQueueService = require('../services/ai-queue'); - -async function testQueue() { - console.log('🧪 Тестирование AI очереди...\n'); - - try { - // Проверяем инициализацию - console.log('1. Проверка инициализации очереди...'); - const stats = aiQueueService.getStats(); - console.log('✅ Очередь инициализирована:', stats.isInitialized); - console.log('📊 Статистика:', { - totalProcessed: stats.totalProcessed, - totalFailed: stats.totalFailed, - currentQueueSize: stats.currentQueueSize, - runningTasks: stats.runningTasks - }); - - // Тестируем добавление задач - console.log('\n2. Тестирование добавления задач...'); - - const testTasks = [ - { - message: 'Привет, как дела?', - language: 'ru', - type: 'chat', - userId: 1, - userRole: 'user', - requestId: 'test_1' - }, - { - message: 'Расскажи о погоде', - language: 'ru', - type: 'analysis', - userId: 1, - userRole: 'user', - requestId: 'test_2' - }, - { - message: 'Срочный вопрос!', - language: 'ru', - type: 'urgent', - userId: 1, - userRole: 'admin', - requestId: 'test_3' - } - ]; - - for (let i = 0; i < testTasks.length; i++) { - const task = testTasks[i]; - console.log(` Добавляем задачу ${i + 1}: "${task.message}"`); - - try { - const result = await aiQueueService.addTask(task); - console.log(` ✅ Задача добавлена, ID: ${result.taskId}`); - } catch (error) { - console.log(` ❌ Ошибка добавления задачи: ${error.message}`); - } - } - - // Ждем обработки - console.log('\n3. Ожидание обработки задач...'); - await new Promise(resolve => setTimeout(resolve, 10000)); - - // Проверяем статистику - console.log('\n4. Проверка статистики после обработки...'); - const finalStats = aiQueueService.getStats(); - console.log('📊 Финальная статистика:', { - totalProcessed: finalStats.totalProcessed, - totalFailed: finalStats.totalFailed, - currentQueueSize: finalStats.currentQueueSize, - runningTasks: finalStats.runningTasks, - averageProcessingTime: Math.round(finalStats.averageProcessingTime) - }); - - // Тестируем управление очередью - console.log('\n5. Тестирование управления очередью...'); - - console.log(' Пауза очереди...'); - aiQueueService.pause(); - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log(' Возобновление очереди...'); - aiQueueService.resume(); - await new Promise(resolve => setTimeout(resolve, 1000)); - - console.log('\n✅ Тестирование завершено!'); - - } catch (error) { - console.error('❌ Ошибка тестирования:', error); - } -} - -// Запуск теста -if (require.main === module) { - testQueue().then(() => { - console.log('\n🏁 Тест завершен'); - process.exit(0); - }).catch(error => { - console.error('💥 Критическая ошибка:', error); - process.exit(1); - }); -} - -module.exports = { testQueue }; \ No newline at end of file diff --git a/backend/scripts/test-encrypted-tables.js b/backend/scripts/test-encrypted-tables.js deleted file mode 100644 index c4276f4..0000000 --- a/backend/scripts/test-encrypted-tables.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - * All rights reserved. - * - * This software is proprietary and confidential. - * Unauthorized copying, modification, or distribution is prohibited. - * - * For licensing inquiries: info@hb3-accelerator.com - * Website: https://hb3-accelerator.com - * GitHub: https://github.com/HB3-ACCELERATOR - */ - -const encryptedDb = require('../services/encryptedDatabaseService'); -const db = require('../db'); - -async function testEncryptedTables() { - console.log('🔐 Тестирование зашифрованных таблиц...\n'); - - try { - // Тестируем таблицу is_rag_source - console.log('1. Тестирование таблицы is_rag_source:'); - const ragSources = await encryptedDb.getData('is_rag_source', {}); - console.log(' ✅ Данные получены:', ragSources); - - // Тестируем через прямой SQL запрос - const fs = require('fs'); - const path = require('path'); - let encryptionKey = 'default-key'; - - try { - const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); - if (fs.existsSync(keyPath)) { - encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); - } - } catch (keyError) { - console.error('Error reading encryption key:', keyError); - } - - const directResult = await db.getQuery()( - 'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id', - [encryptionKey] - ); - console.log(' ✅ Прямой SQL запрос:', directResult.rows); - - // Тестируем другие важные таблицы - console.log('\n2. Тестирование других зашифрованных таблиц:'); - - // user_tables - const userTables = await encryptedDb.getData('user_tables', {}, 5); - console.log(' ✅ user_tables (первые 5):', userTables.length, 'записей'); - - // user_columns - const userColumns = await encryptedDb.getData('user_columns', {}, 5); - console.log(' ✅ user_columns (первые 5):', userColumns.length, 'записей'); - - // messages - const messages = await encryptedDb.getData('messages', {}, 3); - console.log(' ✅ messages (первые 3):', messages.length, 'записей'); - - // conversations - const conversations = await encryptedDb.getData('conversations', {}, 3); - console.log(' ✅ conversations (первые 3):', conversations.length, 'записей'); - - console.log('\n✅ Все тесты прошли успешно!'); - - } catch (error) { - console.error('❌ Ошибка тестирования:', error); - } -} - -// Запуск теста -if (require.main === module) { - testEncryptedTables().then(() => { - console.log('\n🏁 Тест завершен'); - process.exit(0); - }).catch(error => { - console.error('💥 Критическая ошибка:', error); - process.exit(1); - }); -} - -module.exports = { testEncryptedTables }; \ No newline at end of file diff --git a/backend/scripts/verify-with-hardhat-v2.js b/backend/scripts/verify-with-hardhat-v2.js index f8f4bce..f2edf50 100644 --- a/backend/scripts/verify-with-hardhat-v2.js +++ b/backend/scripts/verify-with-hardhat-v2.js @@ -1,13 +1,207 @@ /** - * Верификация контрактов с Hardhat V2 API + * Верификация контрактов в Etherscan V2 */ -const { execSync } = require('child_process'); +// const { execSync } = require('child_process'); // Удалено - больше не используем Hardhat verify const DeployParamsService = require('../services/deployParamsService'); const deploymentWebSocketService = require('../services/deploymentWebSocketService'); +const { getSecret } = require('../services/secretStore'); + +// Функция для определения Etherscan V2 API URL по chainId +function getEtherscanApiUrl(chainId) { + // Используем единый Etherscan V2 API для всех сетей + return `https://api.etherscan.io/v2/api?chainid=${chainId}`; +} + +// Импортируем вспомогательную функцию +const { createStandardJsonInput: createStandardJsonInputHelper } = require('../utils/standardJsonInputHelper'); + +// Функция для создания стандартного JSON input +function createStandardJsonInput() { + const path = require('path'); + const contractPath = path.join(__dirname, '../contracts/DLE.sol'); + return createStandardJsonInputHelper(contractPath, 'DLE'); +} + +// Функция для проверки статуса верификации +async function checkVerificationStatus(chainId, guid, apiKey) { + const apiUrl = getEtherscanApiUrl(chainId); + + const formData = new URLSearchParams({ + apikey: apiKey, + module: 'contract', + action: 'checkverifystatus', + guid: guid + }); + + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }); + + const result = await response.json(); + return result; + } catch (error) { + console.error('❌ Ошибка при проверке статуса:', error.message); + return { status: '0', message: error.message }; + } +} + +// Функция для проверки реального статуса контракта в Etherscan +async function checkContractVerificationStatus(chainId, contractAddress, apiKey) { + const apiUrl = getEtherscanApiUrl(chainId); + + const formData = new URLSearchParams({ + apikey: apiKey, + module: 'contract', + action: 'getsourcecode', + address: contractAddress + }); + + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }); + + const result = await response.json(); + + if (result.status === '1' && result.result && result.result[0]) { + const contractInfo = result.result[0]; + const isVerified = contractInfo.SourceCode && contractInfo.SourceCode !== ''; + + console.log(`🔍 Статус контракта ${contractAddress}:`, { + isVerified: isVerified, + contractName: contractInfo.ContractName || 'Unknown', + compilerVersion: contractInfo.CompilerVersion || 'Unknown' + }); + + return { isVerified, contractInfo }; + } else { + console.log('❌ Не удалось получить информацию о контракте:', result.message); + return { isVerified: false, error: result.message }; + } + } catch (error) { + console.error('❌ Ошибка при проверке статуса контракта:', error.message); + return { isVerified: false, error: error.message }; + } +} + +// Функция для верификации контракта в Etherscan V2 +async function verifyContractInEtherscan(chainId, contractAddress, constructorArgsHex, apiKey) { + const apiUrl = getEtherscanApiUrl(chainId); + const standardJsonInput = createStandardJsonInput(); + + console.log(`🔍 Верификация контракта ${contractAddress} в Etherscan V2 (chainId: ${chainId})`); + console.log(`📡 API URL: ${apiUrl}`); + + const formData = new URLSearchParams({ + apikey: apiKey, + module: 'contract', + action: 'verifysourcecode', + contractaddress: contractAddress, + codeformat: 'solidity-standard-json-input', + contractname: 'DLE.sol:DLE', + sourceCode: JSON.stringify(standardJsonInput), + compilerversion: 'v0.8.20+commit.a1b79de6', + optimizationUsed: '1', + runs: '0', + constructorArguements: constructorArgsHex + }); + + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: formData + }); + + const result = await response.json(); + console.log('📥 Ответ от Etherscan V2:', result); + + if (result.status === '1') { + console.log('✅ Верификация отправлена в Etherscan V2!'); + console.log(`📋 GUID: ${result.result}`); + + // Ждем и проверяем статус верификации с повторными попытками + console.log('⏳ Ждем 15 секунд перед проверкой статуса...'); + await new Promise(resolve => setTimeout(resolve, 15000)); + + // Проверяем статус с повторными попытками (до 3 раз) + let statusResult; + let attempts = 0; + const maxAttempts = 3; + + do { + attempts++; + console.log(`📊 Проверка статуса верификации (попытка ${attempts}/${maxAttempts})...`); + statusResult = await checkVerificationStatus(chainId, result.result, apiKey); + console.log('📊 Статус верификации:', statusResult); + + if (statusResult.status === '1') { + console.log('🎉 Верификация успешна!'); + return { success: true, guid: result.result, message: 'Верифицировано в Etherscan V2' }; + } else if (statusResult.status === '0' && statusResult.result.includes('Pending')) { + console.log('⏳ Верификация в очереди, проверяем реальный статус контракта...'); + + // Проверяем реальный статус контракта в Etherscan + const contractStatus = await checkContractVerificationStatus(chainId, contractAddress, apiKey); + if (contractStatus.isVerified) { + console.log('✅ Контракт уже верифицирован в Etherscan!'); + return { success: true, guid: result.result, message: 'Контракт верифицирован' }; + } else { + console.log('⏳ Контракт еще не верифицирован, ожидаем завершения...'); + if (attempts < maxAttempts) { + console.log(`⏳ Ждем еще 10 секунд перед следующей попыткой...`); + await new Promise(resolve => setTimeout(resolve, 10000)); + } + } + } else { + console.log('❌ Верификация не удалась:', statusResult.result); + return { success: false, error: statusResult.result }; + } + } while (attempts < maxAttempts && statusResult.status === '0' && statusResult.result.includes('Pending')); + + // Если все попытки исчерпаны + if (attempts >= maxAttempts) { + console.log('⏳ Максимальное количество попыток достигнуто, верификация может быть в процессе...'); + return { success: false, error: 'Ожидание верификации', guid: result.result }; + } + } else { + console.log('❌ Ошибка отправки верификации в Etherscan V2:', result.message); + + // Проверяем, не верифицирован ли уже контракт + if (result.message && result.message.includes('already verified')) { + console.log('✅ Контракт уже верифицирован'); + return { success: true, message: 'Контракт уже верифицирован' }; + } + + return { success: false, error: result.message }; + } + } catch (error) { + console.error('❌ Ошибка при отправке запроса в Etherscan V2:', error.message); + + // Проверяем, не является ли это ошибкой сети + if (error.message.includes('fetch') || error.message.includes('network')) { + console.log('⚠️ Ошибка сети, верификация может быть в процессе...'); + return { success: false, error: 'Network error - verification may be in progress' }; + } + + return { success: false, error: error.message }; + } +} async function verifyWithHardhatV2(params = null, deployedNetworks = null) { - console.log('🚀 Запуск верификации с Hardhat V2...'); + console.log('🚀 Запуск верификации контрактов...'); try { // Если параметры не переданы, получаем их из базы данных @@ -23,10 +217,15 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) { params = latestParams[0]; } - if (!params.etherscan_api_key) { - throw new Error('Etherscan API ключ не найден в параметрах'); + // Проверяем API ключ в параметрах или переменной окружения + const etherscanApiKey = params.etherscan_api_key || process.env.ETHERSCAN_API_KEY; + if (!etherscanApiKey) { + throw new Error('Etherscan API ключ не найден в параметрах или переменной окружения'); } + // Устанавливаем API ключ в переменную окружения для использования в коде + process.env.ETHERSCAN_API_KEY = etherscanApiKey; + console.log('📋 Параметры деплоя:', { deploymentId: params.deployment_id, name: params.name, @@ -54,38 +253,35 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) { } console.log(`🌐 Найдено ${networks.length} развернутых сетей`); - // Маппинг chainId на названия сетей - const networkMap = { - 1: 'mainnet', - 11155111: 'sepolia', - 17000: 'holesky', - 137: 'polygon', - 42161: 'arbitrumOne', - 421614: 'arbitrumSepolia', - 56: 'bsc', - 8453: 'base', - 84532: 'baseSepolia' - }; + // Получаем маппинг chainId на названия сетей из параметров деплоя + const networkMap = {}; + if (params.supportedChainIds && params.supportedChainIds.length > 0) { + // Создаем маппинг только для поддерживаемых сетей + for (const chainId of params.supportedChainIds) { + switch (chainId) { + case 1: networkMap[chainId] = 'mainnet'; break; + case 11155111: networkMap[chainId] = 'sepolia'; break; + case 17000: networkMap[chainId] = 'holesky'; break; + case 137: networkMap[chainId] = 'polygon'; break; + case 42161: networkMap[chainId] = 'arbitrumOne'; break; + case 421614: networkMap[chainId] = 'arbitrumSepolia'; break; + case 56: networkMap[chainId] = 'bsc'; break; + case 8453: networkMap[chainId] = 'base'; break; + case 84532: networkMap[chainId] = 'baseSepolia'; break; + default: networkMap[chainId] = `chain-${chainId}`; break; + } + } + } else { + // Fallback для совместимости + networkMap[11155111] = 'sepolia'; + networkMap[17000] = 'holesky'; + networkMap[421614] = 'arbitrumSepolia'; + networkMap[84532] = 'baseSepolia'; + } - // Подготавливаем аргументы конструктора - const constructorArgs = [ - { - name: params.name || '', - symbol: params.symbol || '', - location: params.location || '', - coordinates: params.coordinates || '', - jurisdiction: params.jurisdiction || 0, - oktmo: params.oktmo || '', - okvedCodes: params.okvedCodes || [], - kpp: params.kpp ? params.kpp : 0, - quorumPercentage: params.quorumPercentage || 51, - initialPartners: params.initialPartners || [], - initialAmounts: (params.initialAmounts || []).map(amount => (parseFloat(amount) * 10**18).toString()), - supportedChainIds: (params.supportedChainIds || []).map(id => id.toString()) - }, - (params.currentChainId || params.supportedChainIds?.[0] || 1).toString(), - params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000" - ]; + // Используем централизованный генератор параметров конструктора + const { generateVerificationArgs } = require('../utils/constructorArgsGenerator'); + const constructorArgs = generateVerificationArgs(params); console.log('📊 Аргументы конструктора подготовлены'); @@ -125,77 +321,61 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) { await new Promise(resolve => setTimeout(resolve, 5000)); } - // Создаем временный файл с аргументами конструктора - const fs = require('fs'); - const path = require('path'); - const argsFile = path.join(__dirname, `constructor-args-${Date.now()}.json`); - - try { - fs.writeFileSync(argsFile, JSON.stringify(constructorArgs, null, 2)); - - // Формируем команду верификации с файлом аргументов - const command = `ETHERSCAN_API_KEY="${params.etherscan_api_key}" npx hardhat verify --network ${networkName} ${address} --constructor-args ${argsFile}`; - - console.log(`💻 Выполняем команду: npx hardhat verify --network ${networkName} ${address} --constructor-args ${argsFile}`); - - const output = execSync(command, { - cwd: '/app', - encoding: 'utf8', - stdio: 'pipe' + // Получаем API ключ Etherscan + const etherscanApiKey = process.env.ETHERSCAN_API_KEY; + if (!etherscanApiKey) { + console.log('❌ API ключ Etherscan не найден, пропускаем верификацию в Etherscan'); + verificationResults.push({ + success: false, + network: networkName, + chainId: chainId, + error: 'No Etherscan API key' }); - - console.log('✅ Верификация успешна:'); - console.log(output); - + continue; + } + + // Кодируем аргументы конструктора в hex + const { ethers } = require('ethers'); + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + + // Используем централизованный генератор параметров конструктора + const { generateDeploymentArgs } = require('../utils/constructorArgsGenerator'); + const { dleConfig, initializer } = generateDeploymentArgs(params); + + const encodedArgs = abiCoder.encode( + [ + 'tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, uint256 quorumPercentage, address[] initialPartners, uint256[] initialAmounts, uint256[] supportedChainIds)', + 'address' + ], + [ + dleConfig, + initializer + ] + ); + + const constructorArgsHex = encodedArgs.slice(2); // Убираем 0x + + // Верификация в Etherscan + console.log('🌐 Верификация в Etherscan...'); + const etherscanResult = await verifyContractInEtherscan(chainId, address, constructorArgsHex, etherscanApiKey); + + if (etherscanResult.success) { + console.log('✅ Верификация в Etherscan успешна!'); verificationResults.push({ success: true, network: networkName, - chainId: chainId + chainId: chainId, + etherscan: true, + message: etherscanResult.message + }); + } else { + console.log('❌ Ошибка верификации в Etherscan:', etherscanResult.error); + verificationResults.push({ + success: false, + network: networkName, + chainId: chainId, + error: etherscanResult.error }); - - // Удаляем временный файл - try { - fs.unlinkSync(argsFile); - } catch (e) { - console.log(`⚠️ Не удалось удалить временный файл: ${argsFile}`); - } - - } catch (error) { - // Удаляем временный файл в случае ошибки - try { - fs.unlinkSync(argsFile); - } catch (e) { - console.log(`⚠️ Не удалось удалить временный файл: ${argsFile}`); - } - - const errorOutput = error.stdout || error.stderr || error.message; - console.log('📥 Вывод команды:'); - console.log(errorOutput); - - if (errorOutput.includes('Already Verified')) { - console.log('ℹ️ Контракт уже верифицирован'); - verificationResults.push({ - success: true, - network: networkName, - chainId: chainId, - alreadyVerified: true - }); - } else if (errorOutput.includes('Successfully verified')) { - console.log('✅ Контракт успешно верифицирован!'); - verificationResults.push({ - success: true, - network: networkName, - chainId: chainId - }); - } else { - console.log('❌ Ошибка верификации'); - verificationResults.push({ - success: false, - network: networkName, - chainId: chainId, - error: errorOutput - }); - } } } @@ -203,17 +383,20 @@ async function verifyWithHardhatV2(params = null, deployedNetworks = null) { console.log('\n📊 Итоговые результаты верификации:'); const successful = verificationResults.filter(r => r.success).length; const failed = verificationResults.filter(r => !r.success).length; - const alreadyVerified = verificationResults.filter(r => r.alreadyVerified).length; + const etherscanVerified = verificationResults.filter(r => r.etherscan).length; console.log(`✅ Успешно верифицировано: ${successful}`); - console.log(`ℹ️ Уже было верифицировано: ${alreadyVerified}`); + console.log(`🌐 В Etherscan: ${etherscanVerified}`); console.log(`❌ Ошибки: ${failed}`); verificationResults.forEach(result => { - const status = result.success - ? (result.alreadyVerified ? 'ℹ️' : '✅') - : '❌'; - console.log(`${status} ${result.network} (${result.chainId}): ${result.success ? 'OK' : result.error?.substring(0, 100) + '...'}`); + const status = result.success ? '✅' : '❌'; + + const message = result.success + ? (result.message || 'OK') + : result.error?.substring(0, 100) + '...'; + + console.log(`${status} ${result.network} (${result.chainId}): ${message}`); }); console.log('\n🎉 Верификация завершена!'); @@ -327,13 +510,26 @@ async function verifyModules() { } }; - // Маппинг chainId на названия сетей для Hardhat - const networkMap = { - 11155111: 'sepolia', - 17000: 'holesky', - 421614: 'arbitrumSepolia', - 84532: 'baseSepolia' - }; + // Получаем маппинг chainId на названия сетей из параметров деплоя + const networkMap = {}; + if (params.supportedChainIds && params.supportedChainIds.length > 0) { + // Создаем маппинг только для поддерживаемых сетей + for (const chainId of params.supportedChainIds) { + switch (chainId) { + case 11155111: networkMap[chainId] = 'sepolia'; break; + case 17000: networkMap[chainId] = 'holesky'; break; + case 421614: networkMap[chainId] = 'arbitrumSepolia'; break; + case 84532: networkMap[chainId] = 'baseSepolia'; break; + default: networkMap[chainId] = `chain-${chainId}`; break; + } + } + } else { + // Fallback для совместимости + networkMap[11155111] = 'sepolia'; + networkMap[17000] = 'holesky'; + networkMap[421614] = 'arbitrumSepolia'; + networkMap[84532] = 'baseSepolia'; + } // Верифицируем каждый модуль for (const file of moduleFiles) { @@ -375,29 +571,12 @@ async function verifyModules() { const argsFile = path.join(__dirname, `temp-args-${Date.now()}.json`); fs.writeFileSync(argsFile, JSON.stringify(constructorArgs, null, 2)); - // Выполняем верификацию - const command = `ETHERSCAN_API_KEY="${params.etherscan_api_key}" npx hardhat verify --network ${networkName} ${network.address} --constructor-args ${argsFile}`; - console.log(`📝 Команда верификации: npx hardhat verify --network ${networkName} ${network.address} --constructor-args ${argsFile}`); + // Верификация модулей через Etherscan V2 API (пока не реализовано) + console.log(`⚠️ Верификация модулей через Etherscan V2 API пока не реализована для ${moduleData.moduleType} в ${networkName}`); - try { - const output = execSync(command, { - cwd: '/app', - encoding: 'utf8', - stdio: 'pipe' - }); - console.log(`✅ ${moduleData.moduleType} успешно верифицирован в ${networkName}`); - console.log(output); - - // Уведомляем WebSocket клиентов о успешной верификации - deploymentWebSocketService.addDeploymentLog(dleAddress, 'success', `Модуль ${moduleData.moduleType} верифицирован в ${networkName}`); - deploymentWebSocketService.notifyModuleVerified(dleAddress, moduleData.moduleType, networkName); - } catch (verifyError) { - console.log(`❌ Ошибка верификации ${moduleData.moduleType} в ${networkName}: ${verifyError.message}`); - } finally { - // Удаляем временный файл - if (fs.existsSync(argsFile)) { - fs.unlinkSync(argsFile); - } + // Удаляем временный файл + if (fs.existsSync(argsFile)) { + fs.unlinkSync(argsFile); } } catch (error) { diff --git a/backend/server.js b/backend/server.js index 9641a44..691a122 100644 --- a/backend/server.js +++ b/backend/server.js @@ -87,7 +87,7 @@ async function startServer() { console.log(`✅ Server is running on port ${PORT}`); } -server.listen(PORT, async () => { +server.listen(PORT, '0.0.0.0', async () => { try { await startServer(); } catch (error) { diff --git a/backend/services/authTokenService.js b/backend/services/authTokenService.js index 814b9ea..611cc29 100644 --- a/backend/services/authTokenService.js +++ b/backend/services/authTokenService.js @@ -43,6 +43,11 @@ async function upsertAuthToken(token) { const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold); const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold); + // Валидация порогов доступа + if (readonlyThreshold >= editorThreshold) { + throw new Error('Минимум токенов для Read-Only доступа должен быть меньше минимума для Editor доступа'); + } + console.log('[AuthTokenService] Вычисленные значения:'); console.log('[AuthTokenService] readonlyThreshold:', readonlyThreshold); console.log('[AuthTokenService] editorThreshold:', editorThreshold); diff --git a/backend/services/deployParamsService.js b/backend/services/deployParamsService.js index 2e199f4..656c8b7 100644 --- a/backend/services/deployParamsService.js +++ b/backend/services/deployParamsService.js @@ -38,22 +38,23 @@ class DeployParamsService { coordinates: params.coordinates, jurisdiction: params.jurisdiction, oktmo: params.oktmo, - okved_codes: JSON.stringify(params.okvedCodes || []), + okved_codes: JSON.stringify(params.okved_codes || []), kpp: params.kpp, - quorum_percentage: params.quorumPercentage, - initial_partners: JSON.stringify(params.initialPartners || []), - initial_amounts: JSON.stringify(params.initialAmounts || []), - supported_chain_ids: JSON.stringify(params.supportedChainIds || []), - current_chain_id: params.currentChainId, - logo_uri: params.logoURI, - private_key: params.privateKey, // Будет автоматически зашифрован - etherscan_api_key: params.etherscanApiKey, - auto_verify_after_deploy: params.autoVerifyAfterDeploy || false, - create2_salt: params.CREATE2_SALT, - rpc_urls: JSON.stringify(params.rpcUrls ? (Array.isArray(params.rpcUrls) ? params.rpcUrls : Object.values(params.rpcUrls)) : []), + quorum_percentage: params.quorum_percentage, + initial_partners: JSON.stringify(params.initial_partners || []), + // initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое + initial_amounts: JSON.stringify(params.initial_amounts || []), + supported_chain_ids: JSON.stringify(params.supported_chain_ids || []), + current_chain_id: params.current_chain_id || 1, // По умолчанию Ethereum + logo_uri: params.logo_uri, + private_key: params.private_key, // Будет автоматически зашифрован + etherscan_api_key: params.etherscan_api_key, + auto_verify_after_deploy: params.auto_verify_after_deploy || false, + create2_salt: params.create2_salt, + rpc_urls: JSON.stringify(params.rpc_urls ? (Array.isArray(params.rpc_urls) ? params.rpc_urls : Object.values(params.rpc_urls)) : []), initializer: params.initializer, - dle_address: params.dleAddress, - modules_to_deploy: JSON.stringify(params.modulesToDeploy || []), + dle_address: params.dle_address, + modules_to_deploy: JSON.stringify(params.modules_to_deploy || []), deployment_status: status }; @@ -89,6 +90,16 @@ class DeployParamsService { if (!result || result.length === 0) { logger.warn(`⚠️ Параметры деплоя не найдены: ${deploymentId}`); + logger.warn(`🔍 Тип deploymentId: ${typeof deploymentId}`); + logger.warn(`🔍 Значение deploymentId: "${deploymentId}"`); + + // Попробуем найти все записи для отладки + const allRecords = await encryptedDb.getData('deploy_params', {}); + logger.warn(`🔍 Всего записей в deploy_params: ${allRecords?.length || 0}`); + if (allRecords && allRecords.length > 0) { + logger.warn(`🔍 Последние deployment_id: ${allRecords.map(r => r.deployment_id).slice(-3).join(', ')}`); + } + return null; } @@ -118,6 +129,33 @@ class DeployParamsService { } } + /** + * Получает параметры деплоя по адресу DLE + * @param {string} dleAddress - Адрес DLE контракта + * @returns {Promise} - Параметры деплоя или null + */ + async getDeployParamsByDleAddress(dleAddress) { + try { + logger.info(`📖 Поиск параметров деплоя по адресу DLE: ${dleAddress}`); + + // Используем encryptedDb для поиска по адресу DLE + const result = await encryptedDb.getData('deploy_params', { + dle_address: dleAddress + }); + + if (!result || result.length === 0) { + logger.warn(`⚠️ Параметры деплоя не найдены для адреса: ${dleAddress}`); + return null; + } + + // Возвращаем первый найденный результат + return result[0]; + } catch (error) { + logger.error(`❌ Ошибка при поиске параметров деплоя по адресу: ${error.message}`); + throw error; + } + } + /** * Обновляет статус деплоя * @param {string} deploymentId - Идентификатор деплоя @@ -125,25 +163,66 @@ class DeployParamsService { * @param {string} dleAddress - Адрес задеплоенного контракта * @returns {Promise} - Обновленные параметры */ - async updateDeploymentStatus(deploymentId, status, dleAddress = null) { + async updateDeploymentStatus(deploymentId, status, result = null) { try { logger.info(`🔄 Обновление статуса деплоя: ${deploymentId} -> ${status}`); + // Подготавливаем данные для обновления + let dleAddress = null; + let deployResult = null; + + if (result) { + logger.info(`🔍 [DEBUG] updateDeploymentStatus получил result:`, JSON.stringify(result, null, 2)); + + // Извлекаем адреса из результата деплоя + if (result.data && result.data.networks && result.data.networks.length > 0) { + // Берем первый адрес для обратной совместимости + dleAddress = result.data.networks[0].address; + logger.info(`✅ [DEBUG] Найден адрес в result.data.networks[0].address: ${dleAddress}`); + } else if (result.networks && result.networks.length > 0) { + // Берем первый адрес для обратной совместимости + dleAddress = result.networks[0].address; + logger.info(`✅ [DEBUG] Найден адрес в result.networks[0].address: ${dleAddress}`); + } else if (result.output) { + // Ищем адрес в тексте output - сначала пробуем найти JSON массив с адресами + const jsonArrayMatch = result.output.match(/\[[\s\S]*?"address":\s*"(0x[a-fA-F0-9]{40})"[\s\S]*?\]/); + if (jsonArrayMatch) { + dleAddress = jsonArrayMatch[1]; + logger.info(`✅ [DEBUG] Найден адрес в JSON массиве result.output: ${dleAddress}`); + } else { + // Fallback: ищем адрес в тексте output (формат: "📍 Адрес: 0x...") + const addressMatch = result.output.match(/📍 Адрес: (0x[a-fA-F0-9]{40})/); + if (addressMatch) { + dleAddress = addressMatch[1]; + logger.info(`✅ [DEBUG] Найден адрес в тексте result.output: ${dleAddress}`); + } + } + } else { + logger.warn(`⚠️ [DEBUG] Адрес не найден в результате деплоя`); + } + + // Сохраняем полный результат деплоя (включая все адреса всех сетей) + deployResult = JSON.stringify(result); + } + const query = ` UPDATE deploy_params - SET deployment_status = $2, dle_address = $3, updated_at = CURRENT_TIMESTAMP + SET deployment_status = $2, dle_address = $3, deploy_result = $4, updated_at = CURRENT_TIMESTAMP WHERE deployment_id = $1 RETURNING * `; - const result = await this.pool.query(query, [deploymentId, status, dleAddress]); + const queryResult = await this.pool.query(query, [deploymentId, status, dleAddress, deployResult]); - if (result.rows.length === 0) { + if (queryResult.rows.length === 0) { throw new Error(`Параметры деплоя не найдены: ${deploymentId}`); } logger.info(`✅ Статус деплоя обновлен: ${deploymentId} -> ${status}`); - return result.rows[0]; + if (dleAddress) { + logger.info(`📍 Адрес DLE контракта: ${dleAddress}`); + } + return queryResult.rows[0]; } catch (error) { logger.error(`❌ Ошибка при обновлении статуса деплоя: ${error.message}`); throw error; @@ -212,6 +291,229 @@ class DeployParamsService { } } + /** + * Получить контракты по chainId + */ + async getContractsByChainId(chainId) { + try { + console.log(`[DeployParamsService] Ищем контракты с current_chain_id: ${chainId}`); + + const query = ` + SELECT + deployment_id, + name, + dle_address, + current_chain_id, + supported_chain_ids, + created_at + FROM deploy_params + WHERE current_chain_id = $1 AND dle_address IS NOT NULL + ORDER BY created_at DESC + `; + + const result = await this.pool.query(query, [chainId]); + + console.log(`[DeployParamsService] Найдено контрактов: ${result.rows.length}`); + + return result.rows.map(row => ({ + deploymentId: row.deployment_id, + name: row.name, + dleAddress: row.dle_address, + currentChainId: row.current_chain_id, + supportedChainIds: row.supported_chain_ids, + createdAt: row.created_at + })); + + } catch (error) { + console.error(`[DeployParamsService] Ошибка поиска контрактов по chainId:`, error); + throw error; + } + } + + /** + * Получает все деплои + * @param {number} limit - Количество записей + * @returns {Promise} - Список всех деплоев + */ + async getAllDeployments(limit = 50) { + try { + logger.info(`📋 Получение всех деплоев (лимит: ${limit})`); + + // Используем encryptedDb для автоматического расшифрования + const result = await encryptedDb.getData('deploy_params', {}, limit, 'created_at DESC'); + + return result.map(row => { + // Парсим deployResult для извлечения адресов всех сетей + let deployedNetworks = []; + console.log(`🔍 [DEBUG] Processing deployment ${row.deployment_id}, deploy_result exists:`, !!row.deploy_result); + console.log(`🔍 [DEBUG] deploy_result type:`, typeof row.deploy_result); + if (row.deploy_result) { + try { + const deployResult = typeof row.deploy_result === 'string' + ? JSON.parse(row.deploy_result) + : row.deploy_result; + + console.log(`🔍 [DEBUG] deployResult keys:`, Object.keys(deployResult)); + console.log(`🔍 [DEBUG] deployResult.output exists:`, !!deployResult.output); + console.log(`🔍 [DEBUG] deployResult.data exists:`, !!deployResult.data); + console.log(`🔍 [DEBUG] deployResult.networks exists:`, !!deployResult.networks); + if (deployResult.error) { + console.log(`🔍 [DEBUG] deployResult.error:`, deployResult.error); + } + + // Функция для получения правильного названия сети + const getNetworkName = (chainId) => { + const networkNames = { + 1: 'Ethereum Mainnet', + 11155111: 'Sepolia', + 17000: 'Holesky', + 421614: 'Arbitrum Sepolia', + 84532: 'Base Sepolia', + 137: 'Polygon', + 56: 'BSC', + 42161: 'Arbitrum One' + }; + return networkNames[chainId] || `Chain ${chainId}`; + }; + + // Функция для загрузки ABI для конкретной сети + const loadABIForNetwork = (chainId) => { + try { + const fs = require('fs'); + const path = require('path'); + const abiPath = path.join(__dirname, '../../../frontend/src/utils/dle-abi.js'); + + if (fs.existsSync(abiPath)) { + const abiContent = fs.readFileSync(abiPath, 'utf8'); + // Используем более простое регулярное выражение + const abiMatch = abiContent.match(/export const DLE_ABI = (\[[\s\S]*?\]);/); + if (abiMatch) { + // Попробуем исправить JSON, заменив проблемные символы + let abiText = abiMatch[1]; + // Убираем лишние запятые в конце + abiText = abiText.replace(/,(\s*[}\]])/g, '$1'); + try { + return JSON.parse(abiText); + } catch (parseError) { + console.warn(`⚠️ Ошибка парсинга ABI JSON для сети ${chainId}:`, parseError.message); + // Возвращаем пустой массив как fallback + return []; + } + } + } + } catch (abiError) { + console.warn(`⚠️ Ошибка загрузки ABI для сети ${chainId}:`, abiError.message); + } + return null; + }; + + // Извлекаем адреса из результата деплоя + if (deployResult.data && deployResult.data.networks) { + deployedNetworks = deployResult.data.networks.map(network => ({ + chainId: network.chainId, + address: network.address, + networkName: network.networkName || getNetworkName(network.chainId), + abi: loadABIForNetwork(network.chainId) // ABI для каждой сети отдельно + })); + } else if (deployResult.networks) { + deployedNetworks = deployResult.networks.map(network => ({ + chainId: network.chainId, + address: network.address, + networkName: network.networkName || getNetworkName(network.chainId), + abi: loadABIForNetwork(network.chainId) // ABI для каждой сети отдельно + })); + } else if (deployResult.output) { + console.log(`🔍 [DEBUG] Processing deployResult.output`); + // Извлекаем адреса из текста output + const output = deployResult.output; + const addressMatches = output.match(/📍 Адрес: (0x[a-fA-F0-9]{40})/g); + const chainIdMatches = output.match(/chainId: (\d+)/g); + + // Альтернативный поиск по названиям сетей + const networkMatches = output.match(/🔍 Верификация в сети (\w+) \(chainId: (\d+)\)/g); + + console.log(`🔍 [DEBUG] addressMatches:`, addressMatches); + console.log(`🔍 [DEBUG] chainIdMatches:`, chainIdMatches); + console.log(`🔍 [DEBUG] networkMatches:`, networkMatches); + + if (networkMatches && networkMatches.length > 0) { + // Используем networkMatches для более точного парсинга + deployedNetworks = networkMatches.map((match) => { + const [, networkName, chainIdStr] = match.match(/🔍 Верификация в сети (\w+) \(chainId: (\d+)\)/); + const chainId = parseInt(chainIdStr); + + // Ищем адрес для этой сети в output + const addressRegex = new RegExp(`🔍 Верификация в сети ${networkName} \\(chainId: ${chainId}\\)\\n📍 Адрес: (0x[a-fA-F0-9]{40})`); + const addressMatch = output.match(addressRegex); + const address = addressMatch ? addressMatch[1] : '0x0000000000000000000000000000000000000000'; + + return { + chainId: chainId, + address: address, + networkName: getNetworkName(chainId), + abi: loadABIForNetwork(chainId) + }; + }); + console.log(`🔍 [DEBUG] deployedNetworks created from networkMatches:`, deployedNetworks); + } else if (addressMatches && chainIdMatches) { + deployedNetworks = addressMatches.map((match, index) => { + const address = match.match(/📍 Адрес: (0x[a-fA-F0-9]{40})/)[1]; + const chainId = chainIdMatches[index] ? parseInt(chainIdMatches[index].match(/chainId: (\d+)/)[1]) : null; + + return { + chainId: chainId, + address: address, + networkName: chainId ? getNetworkName(chainId) : `Network ${index + 1}`, + abi: loadABIForNetwork(chainId) // ABI для каждой сети отдельно + }; + }); + console.log(`🔍 [DEBUG] deployedNetworks created:`, deployedNetworks); + } else { + console.log(`🔍 [DEBUG] No matches found - addressMatches:`, !!addressMatches, 'chainIdMatches:', !!chainIdMatches); + } + } + } catch (error) { + logger.warn(`⚠️ Ошибка парсинга deployResult для ${row.deployment_id}: ${error.message}`); + } + } + + return { + deploymentId: row.deployment_id, + name: row.name, + symbol: row.symbol, + location: row.location, + coordinates: row.coordinates, + jurisdiction: row.jurisdiction, + oktmo: row.oktmo, + okvedCodes: row.okved_codes || [], + kpp: row.kpp, + quorumPercentage: row.quorum_percentage, + initialPartners: row.initial_partners || [], + initialAmounts: row.initial_amounts || [], + supportedChainIds: row.supported_chain_ids || [], + currentChainId: row.current_chain_id, + logoURI: row.logo_uri, + etherscanApiKey: row.etherscan_api_key, + autoVerifyAfterDeploy: row.auto_verify_after_deploy, + create2Salt: row.create2_salt, + rpcUrls: row.rpc_urls || [], + initializer: row.initializer, + dleAddress: row.dle_address, + modulesToDeploy: row.modules_to_deploy || [], + deploymentStatus: row.deployment_status, + deployResult: row.deploy_result, + deployedNetworks: deployedNetworks, // Добавляем адреса всех сетей + createdAt: row.created_at, + updatedAt: row.updated_at + }; + }); + + } catch (error) { + logger.error(`❌ Ошибка при получении всех деплоев: ${error.message}`); + throw error; + } + } + /** * Закрывает соединение с базой данных */ diff --git a/backend/services/deploymentWebSocketService.js b/backend/services/deploymentWebSocketService.js index 3f3e54e..e7426a4 100644 --- a/backend/services/deploymentWebSocketService.js +++ b/backend/services/deploymentWebSocketService.js @@ -90,6 +90,11 @@ class DeploymentWebSocketService { this.clients.get(ws.dleAddress).delete(ws); if (this.clients.get(ws.dleAddress).size === 0) { this.clients.delete(ws.dleAddress); + // Очищаем сессию деплоя если нет активных клиентов + if (this.deploymentSessions.has(ws.dleAddress)) { + console.log(`[DeploymentWS] Очистка сессии деплоя для DLE: ${ws.dleAddress}`); + this.deploymentSessions.delete(ws.dleAddress); + } } } } diff --git a/backend/services/dleV2Service.js b/backend/services/dleV2Service.js index c0d4ec5..78876d6 100644 --- a/backend/services/dleV2Service.js +++ b/backend/services/dleV2Service.js @@ -37,7 +37,6 @@ class DLEV2Service { * @returns {Promise} - Результат создания DLE */ async createDLE(dleParams, deploymentId = null) { - console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!"); logger.info("🚀 Начало создания DLE v2 с параметрами:", dleParams); try { @@ -46,7 +45,6 @@ class DLEV2Service { deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; } - console.log(`🆔 Deployment ID: ${deploymentId}`); logger.info(`🆔 Deployment ID: ${deploymentId}`); // WebSocket обновление: начало процесса @@ -58,21 +56,13 @@ class DLEV2Service { this.validateDLEParams(dleParams); // Подготовка параметров для деплоя - console.log('🔧 Подготавливаем параметры для деплоя...'); logger.info('🔧 Подготавливаем параметры для деплоя...'); - // Отладка: проверяем входные параметры - console.log('🔍 ОТЛАДКА - Входные параметры:'); - console.log(' supportedChainIds:', JSON.stringify(dleParams.supportedChainIds, null, 2)); - console.log(' privateKey:', dleParams.privateKey ? '[ЕСТЬ]' : '[НЕТ]'); - console.log(' name:', dleParams.name); - const deployParams = this.prepareDeployParams(dleParams); - console.log('✅ Параметры подготовлены:', JSON.stringify(deployParams, null, 2)); - logger.info('✅ Параметры подготовлены:', JSON.stringify(deployParams, null, 2)); + logger.info('✅ Параметры подготовлены'); // Сохраняем подготовленные параметры в базу данных - logger.info(`💾 Сохранение подготовленных параметров деплоя в БД: ${deploymentId}`); + logger.info(`💾 Сохранение параметров деплоя в БД: ${deploymentId}`); await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending'); // Вычисляем адрес инициализатора @@ -84,27 +74,17 @@ class DLEV2Service { logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message); } - // WebSocket обновление: генерация CREATE2_SALT + // WebSocket обновление: подготовка к деплою if (deploymentId) { - deploymentTracker.updateProgress(deploymentId, 'Генерация CREATE2 SALT', 10, 'Создаем уникальный идентификатор для детерминированного адреса'); + deploymentTracker.updateProgress(deploymentId, 'Подготовка к деплою', 10, 'Настраиваем параметры для детерминированного деплоя'); } - // Генерируем одноразовый CREATE2_SALT - const { createAndStoreNewCreate2Salt } = require('./secretStore'); - const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' }); - logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`); - - // Обновляем параметры в базе данных с CREATE2_SALT - console.log('💾 Обновляем параметры в базе данных с CREATE2_SALT...'); - logger.info('💾 Обновляем параметры в базе данных с CREATE2_SALT...'); + // Обновляем параметры в базе данных + console.log('💾 Обновляем параметры в базе данных...'); + logger.info('💾 Обновляем параметры в базе данных...'); - const updatedParams = { - ...deployParams, - CREATE2_SALT: create2Salt - }; - - await this.deployParamsService.saveDeployParams(deploymentId, updatedParams, 'in_progress'); - logger.info(`✅ Параметры обновлены в БД с CREATE2_SALT: ${create2Salt}`); + await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'in_progress'); + logger.info(`✅ Параметры обновлены в БД для деплоя`); // WebSocket обновление: поиск RPC URLs if (deploymentId) { @@ -153,6 +133,8 @@ class DLEV2Service { // Обновляем параметры в базе данных с RPC URLs и initializer const finalParams = { ...updatedParams, + // Сохраняем initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое + initialAmounts: dleParams.initialAmounts, rpcUrls: rpcUrls, // Сохраняем как объект {chainId: url} rpc_urls: Object.values(rpcUrls), // Также сохраняем как массив для совместимости initializer: dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000" @@ -203,7 +185,18 @@ class DLEV2Service { const result = this.extractDeployResult(deployResult.stdout, deployParams); if (!result || !result.success) { - throw new Error('Деплой не удался: ' + (result?.error || 'Неизвестная ошибка')); + // Логируем детали ошибки для отладки + logger.error('❌ Деплой не удался. Детали:'); + logger.error(`📋 stdout: ${deployResult.stdout}`); + logger.error(`📋 stderr: ${deployResult.stderr}`); + logger.error(`📋 exitCode: ${deployResult.exitCode}`); + + // Извлекаем конкретную ошибку из результата + const errorMessage = result?.error || + deployResult.stderr || + 'Неизвестная ошибка'; + + throw new Error(`Деплой не удался: ${errorMessage}`); } // Сохраняем данные DLE @@ -218,8 +211,11 @@ class DLEV2Service { // Обновляем статус деплоя в базе данных if (deploymentId && result.data.dleAddress) { + logger.info(`🔄 Обновляем адрес в БД: ${deploymentId} -> ${result.data.dleAddress}`); await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result.data.dleAddress); - logger.info(`✅ Статус деплоя обновлен в БД: ${deploymentId} -> completed`); + logger.info(`✅ Статус деплоя обновлен в БД: ${deploymentId} -> completed, адрес: ${result.data.dleAddress}`); + } else { + logger.warn(`⚠️ Не удалось обновить адрес в БД: deploymentId=${deploymentId}, dleAddress=${result.data?.dleAddress}`); } // WebSocket обновление: финализация @@ -411,14 +407,18 @@ class DLEV2Service { * @returns {Object|null} - Результат деплоя */ extractDeployResult(stdout, deployParams = null) { + logger.info(`🔍 Анализируем вывод деплоя (${stdout.length} символов)`); + // Ищем MULTICHAIN_DEPLOY_RESULT в выводе const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/); if (resultMatch) { try { const deployResults = JSON.parse(resultMatch[1]); + logger.info(`📊 Результаты деплоя: ${JSON.stringify(deployResults, null, 2)}`); // Проверяем, что есть успешные деплои const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000'); + logger.info(`✅ Успешные деплои: ${successfulDeploys.length}, адреса: ${successfulDeploys.map(d => d.address).join(', ')}`); if (successfulDeploys.length > 0) { return { @@ -442,6 +442,54 @@ class DLEV2Service { } catch (e) { logger.error('Ошибка парсинга JSON результата:', e); } + } else { + // Если MULTICHAIN_DEPLOY_RESULT не найден, ищем другие индикаторы успеха + logger.warn('⚠️ MULTICHAIN_DEPLOY_RESULT не найден в выводе'); + + // Ищем индикаторы успешного деплоя + const successIndicators = [ + 'DLE deployment completed successfully', + 'SUCCESS: All DLE addresses are identical', + 'deployed at=', + 'deployment SUCCESS' + ]; + + const hasSuccessIndicator = successIndicators.some(indicator => + stdout.includes(indicator) + ); + + if (hasSuccessIndicator) { + logger.info('✅ Найден индикатор успешного деплоя'); + + // Ищем адреса контрактов в выводе + const addressMatch = stdout.match(/deployed at=([0-9a-fA-Fx]+)/); + if (addressMatch) { + const contractAddress = addressMatch[1]; + logger.info(`✅ Найден адрес контракта: ${contractAddress}`); + + return { + success: true, + data: { + dleAddress: contractAddress, + totalNetworks: 1, + successfulNetworks: 1, + // Добавляем данные из параметров деплоя + name: deployParams?.name || 'Unknown', + symbol: deployParams?.symbol || 'UNK', + location: deployParams?.location || 'Не указан', + coordinates: deployParams?.coordinates || '0,0', + jurisdiction: deployParams?.jurisdiction || 0, + quorumPercentage: deployParams?.quorumPercentage || 51, + logoURI: deployParams?.logoURI || '/uploads/logos/default-token.svg' + } + }; + } + } + + // Логируем последние строки вывода для отладки + const lines = stdout.split('\n'); + const lastLines = lines.slice(-10).join('\n'); + logger.info(`📋 Последние строки вывода:\n${lastLines}`); } return null; diff --git a/backend/services/unifiedDeploymentService.js b/backend/services/unifiedDeploymentService.js new file mode 100644 index 0000000..5f856d9 --- /dev/null +++ b/backend/services/unifiedDeploymentService.js @@ -0,0 +1,523 @@ +/** + * Единый сервис для управления деплоем DLE + * Объединяет все операции с данными и деплоем + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const logger = require('../utils/logger'); +const DeployParamsService = require('./deployParamsService'); +const deploymentTracker = require('../utils/deploymentTracker'); +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const etherscanV2 = require('./etherscanV2VerificationService'); +const { getRpcUrlByChainId } = require('./rpcProviderService'); +const { ethers } = require('ethers'); +// Убираем прямой импорт broadcastDeploymentUpdate - используем только deploymentTracker + +class UnifiedDeploymentService { + constructor() { + this.deployParamsService = new DeployParamsService(); + } + + /** + * Создает новый деплой DLE с полным циклом + * @param {Object} dleParams - Параметры DLE из формы + * @param {string} deploymentId - ID деплоя (опционально) + * @returns {Promise} - Результат деплоя + */ + async createDLE(dleParams, deploymentId = null) { + try { + // 1. Генерируем ID деплоя + if (!deploymentId) { + deploymentId = `deploy_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; + } + + logger.info(`🚀 Начало создания DLE: ${deploymentId}`); + + // 2. Валидируем параметры + this.validateDLEParams(dleParams); + + // 3. Подготавливаем параметры для деплоя + const deployParams = await this.prepareDeployParams(dleParams); + + // 4. Сохраняем в БД + await this.deployParamsService.saveDeployParams(deploymentId, deployParams, 'pending'); + logger.info(`💾 Параметры сохранены в БД: ${deploymentId}`); + + // 5. Запускаем деплой + const result = await this.executeDeployment(deploymentId); + + // 6. Сохраняем результат + await this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result); + logger.info(`✅ Деплой завершен: ${deploymentId}`); + + return { + success: true, + deploymentId, + data: result + }; + + } catch (error) { + logger.error(`❌ Ошибка деплоя ${deploymentId}:`, error); + + // Обновляем статус на ошибку + if (deploymentId) { + await this.deployParamsService.updateDeploymentStatus(deploymentId, 'failed', { error: error.message }); + } + + throw error; + } + } + + /** + * Валидирует параметры DLE + * @param {Object} params - Параметры для валидации + */ + validateDLEParams(params) { + const required = ['name', 'symbol', 'privateKey', 'supportedChainIds']; + const missing = required.filter(field => !params[field]); + + if (missing.length > 0) { + throw new Error(`Отсутствуют обязательные поля: ${missing.join(', ')}`); + } + + if (params.quorumPercentage < 1 || params.quorumPercentage > 100) { + throw new Error('Кворум должен быть от 1 до 100 процентов'); + } + + if (!params.initialPartners || params.initialPartners.length === 0) { + throw new Error('Необходимо указать хотя бы одного партнера'); + } + + if (!params.initialAmounts || params.initialAmounts.length === 0) { + throw new Error('Необходимо указать начальные суммы для партнеров'); + } + + if (params.initialPartners.length !== params.initialAmounts.length) { + throw new Error('Количество партнеров должно совпадать с количеством сумм'); + } + + if (!params.supportedChainIds || params.supportedChainIds.length === 0) { + throw new Error('Необходимо указать поддерживаемые сети'); + } + } + + /** + * Подготавливает параметры для деплоя + * @param {Object} dleParams - Исходные параметры + * @returns {Promise} - Подготовленные параметры + */ + async prepareDeployParams(dleParams) { + // Генерируем RPC URLs на основе supportedChainIds из базы данных + const rpcUrls = await this.generateRpcUrls(dleParams.supportedChainIds || []); + + return { + name: dleParams.name, + symbol: dleParams.symbol, + location: dleParams.location || '', + coordinates: dleParams.coordinates || '', + jurisdiction: dleParams.jurisdiction || 1, + oktmo: dleParams.oktmo || 45000000000, + okved_codes: dleParams.okvedCodes || [], + kpp: dleParams.kpp || 770101001, + quorum_percentage: dleParams.quorumPercentage || 51, + initial_partners: dleParams.initialPartners || [], + // initialAmounts в человекочитаемом формате, умножение на 1e18 происходит при деплое + initial_amounts: dleParams.initialAmounts || [], + supported_chain_ids: dleParams.supportedChainIds || [], + current_chain_id: 1, // Governance chain всегда Ethereum + private_key: dleParams.privateKey, + etherscan_api_key: dleParams.etherscanApiKey, + logo_uri: dleParams.logoURI || '', + create2_salt: dleParams.CREATE2_SALT || `0x${Math.random().toString(16).substring(2).padStart(64, '0')}`, + auto_verify_after_deploy: dleParams.autoVerifyAfterDeploy || false, + modules_to_deploy: dleParams.modulesToDeploy || [], + rpc_urls: rpcUrls, + deployment_status: 'pending' + }; + } + + /** + * Генерирует RPC URLs на основе chain IDs из базы данных + * @param {Array} chainIds - Массив chain IDs + * @returns {Promise} - Массив RPC URLs + */ + async generateRpcUrls(chainIds) { + const { getRpcUrlByChainId } = require('./rpcProviderService'); + const rpcUrls = []; + + for (const chainId of chainIds) { + try { + const rpcUrl = await getRpcUrlByChainId(chainId); + if (rpcUrl) { + rpcUrls.push(rpcUrl); + logger.info(`[RPC_GEN] Найден RPC для chainId ${chainId}: ${rpcUrl}`); + } else { + logger.warn(`[RPC_GEN] RPC не найден для chainId ${chainId}`); + } + } catch (error) { + logger.error(`[RPC_GEN] Ошибка получения RPC для chainId ${chainId}:`, error.message); + } + } + + return rpcUrls; + } + + /** + * Выполняет деплой контрактов + * @param {string} deploymentId - ID деплоя + * @returns {Promise} - Результат деплоя + */ + async executeDeployment(deploymentId) { + return new Promise((resolve, reject) => { + const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js'); + + logger.info(`🚀 Запуск деплоя: ${scriptPath}`); + + const child = spawn('npx', ['hardhat', 'run', scriptPath], { + cwd: path.join(__dirname, '..'), + env: { + ...process.env, + DEPLOYMENT_ID: deploymentId + }, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + const output = data.toString(); + stdout += output; + logger.info(`[DEPLOY] ${output.trim()}`); + + // Определяем этап процесса по содержимому вывода + let progress = 50; + let message = 'Деплой в процессе...'; + + if (output.includes('Генерация ABI файла')) { + progress = 10; + message = 'Генерация ABI файла...'; + } else if (output.includes('Генерация flattened контракта')) { + progress = 20; + message = 'Генерация flattened контракта...'; + } else if (output.includes('Compiled') && output.includes('Solidity files')) { + progress = 30; + message = 'Компиляция контрактов...'; + } else if (output.includes('Загружены параметры')) { + progress = 40; + message = 'Загрузка параметров деплоя...'; + } else if (output.includes('deploying DLE directly')) { + progress = 60; + message = 'Деплой контрактов в сети...'; + } else if (output.includes('Верификация в сети')) { + progress = 80; + message = 'Верификация контрактов...'; + } + + // Отправляем WebSocket сообщение о прогрессе через deploymentTracker + deploymentTracker.updateDeployment(deploymentId, { + status: 'in_progress', + progress: progress, + message: message, + output: output.trim() + }); + }); + + child.stderr.on('data', (data) => { + const output = data.toString(); + stderr += output; + logger.error(`[DEPLOY ERROR] ${output.trim()}`); + }); + + child.on('close', (code) => { + if (code === 0) { + try { + const result = this.parseDeployResult(stdout); + + // Сохраняем результат в БД + this.deployParamsService.updateDeploymentStatus(deploymentId, 'completed', result) + .then(() => { + logger.info(`✅ Результат деплоя сохранен в БД: ${deploymentId}`); + + // Отправляем WebSocket сообщение о завершении через deploymentTracker + deploymentTracker.completeDeployment(deploymentId, result); + + resolve(result); + }) + .catch(dbError => { + logger.error(`❌ Ошибка сохранения результата в БД: ${dbError.message}`); + resolve(result); // Все равно возвращаем результат + }); + } catch (error) { + reject(new Error(`Ошибка парсинга результата: ${error.message}`)); + } + } else { + // Логируем детали ошибки для отладки + logger.error(`❌ Деплой завершился с ошибкой (код ${code})`); + logger.error(`📋 stdout: ${stdout}`); + logger.error(`📋 stderr: ${stderr}`); + + // Извлекаем конкретную ошибку из вывода + const errorMessage = stderr || stdout || 'Неизвестная ошибка'; + + // Отправляем WebSocket сообщение об ошибке через deploymentTracker + deploymentTracker.failDeployment(deploymentId, new Error(`Деплой завершился с ошибкой (код ${code}): ${errorMessage}`)); + + reject(new Error(`Деплой завершился с ошибкой (код ${code}): ${errorMessage}`)); + } + }); + + child.on('error', (error) => { + reject(new Error(`Ошибка запуска деплоя: ${error.message}`)); + }); + }); + } + + /** + * Парсит результат деплоя из вывода скрипта + * @param {string} stdout - Вывод скрипта + * @returns {Object} - Структурированный результат + */ + parseDeployResult(stdout) { + try { + logger.info(`🔍 Анализируем вывод деплоя (${stdout.length} символов)`); + + // Ищем MULTICHAIN_DEPLOY_RESULT в выводе + const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(.+)/); + if (resultMatch) { + const jsonStr = resultMatch[1].trim(); + const deployResults = JSON.parse(jsonStr); + logger.info(`📊 Результаты деплоя: ${JSON.stringify(deployResults, null, 2)}`); + + // Проверяем, что есть успешные деплои + const successfulDeploys = deployResults.filter(r => r.address && r.address !== '0x0000000000000000000000000000000000000000' && !r.error); + + if (successfulDeploys.length > 0) { + const dleAddress = successfulDeploys[0].address; + logger.info(`✅ DLE адрес: ${dleAddress}`); + + return { + success: true, + data: { + dleAddress: dleAddress, + networks: deployResults.map(result => ({ + chainId: result.chainId, + address: result.address, + success: result.address && result.address !== '0x0000000000000000000000000000000000000000' && !result.error, + error: result.error || null, + verification: result.verification || 'pending' + })) + }, + message: `DLE успешно развернут в ${successfulDeploys.length} сетях` + }; + } else { + // Если нет успешных деплоев, но есть результаты, возвращаем их с ошибками + const failedDeploys = deployResults.filter(r => r.error); + logger.warn(`⚠️ Все деплои неудачны. Ошибки: ${failedDeploys.map(d => d.error).join(', ')}`); + + return { + success: false, + data: { + networks: deployResults.map(result => ({ + chainId: result.chainId, + address: result.address || null, + success: false, + error: result.error || 'Unknown error' + })) + }, + message: `Деплой неудачен во всех сетях. Ошибки: ${failedDeploys.map(d => d.error).join(', ')}` + }; + } + } + + // Fallback: создаем результат из текста + return { + success: true, + message: 'Деплой выполнен успешно', + output: stdout + }; + } catch (error) { + logger.error('❌ Ошибка парсинга результата деплоя:', error); + throw new Error(`Не удалось распарсить результат деплоя: ${error.message}`); + } + } + + /** + * Получает статус деплоя + * @param {string} deploymentId - ID деплоя + * @returns {Object} - Статус деплоя + */ + async getDeploymentStatus(deploymentId) { + return await this.deployParamsService.getDeployParams(deploymentId); + } + + /** + * Получает все деплои + * @returns {Array} - Список деплоев + */ + async getAllDeployments() { + return await this.deployParamsService.getAllDeployments(); + } + + /** + * Получает все DLE из файлов (для совместимости) + * @returns {Array} - Список DLE + */ + getAllDLEs() { + try { + const dlesDir = path.join(__dirname, '../contracts-data/dles'); + if (!fs.existsSync(dlesDir)) { + return []; + } + + const files = fs.readdirSync(dlesDir); + const dles = []; + + for (const file of files) { + if (file.includes('dle-v2-') && file.endsWith('.json')) { + const filePath = path.join(dlesDir, file); + try { + const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8')); + if (dleData.dleAddress) { + dles.push(dleData); + } + } catch (err) { + logger.error(`Ошибка при чтении файла ${file}:`, err); + } + } + } + + return dles; + } catch (error) { + logger.error('Ошибка при получении списка DLE:', error); + return []; + } + } + + /** + * Автоматическая верификация контрактов во всех сетях + * @param {Object} params - Параметры верификации + * @returns {Promise} - Результат верификации + */ + async autoVerifyAcrossChains({ deployParams, deployResult, apiKey }) { + try { + logger.info('🔍 Начинаем автоматическую верификацию контрактов'); + + if (!deployResult?.data?.networks) { + throw new Error('Нет данных о сетях для верификации'); + } + + const verificationResults = []; + + for (const network of deployResult.data.networks) { + try { + logger.info(`🔍 Верификация в сети ${network.chainId}...`); + + const result = await etherscanV2.verifyContract({ + contractAddress: network.dleAddress, + chainId: network.chainId, + deployParams, + apiKey + }); + + verificationResults.push({ + chainId: network.chainId, + address: network.dleAddress, + success: result.success, + guid: result.guid, + message: result.message + }); + + logger.info(`✅ Верификация в сети ${network.chainId} завершена`); + } catch (error) { + logger.error(`❌ Ошибка верификации в сети ${network.chainId}:`, error); + verificationResults.push({ + chainId: network.chainId, + address: network.dleAddress, + success: false, + error: error.message + }); + } + } + + return { + success: true, + results: verificationResults + }; + } catch (error) { + logger.error('❌ Ошибка автоматической верификации:', error); + throw error; + } + } + + /** + * Проверяет балансы в указанных сетях + * @param {Array} chainIds - Список ID сетей + * @param {string} privateKey - Приватный ключ + * @returns {Promise} - Результат проверки + */ + async checkBalances(chainIds, privateKey) { + try { + logger.info(`💰 Проверка балансов в ${chainIds.length} сетях`); + + const wallet = new ethers.Wallet(privateKey); + const results = []; + + for (const chainId of chainIds) { + try { + const rpcUrl = await getRpcUrlByChainId(chainId); + if (!rpcUrl) { + results.push({ + chainId, + success: false, + error: `RPC URL не найден для сети ${chainId}` + }); + continue; + } + + // Убеждаемся, что rpcUrl - это строка + const rpcUrlString = typeof rpcUrl === 'string' ? rpcUrl : rpcUrl.toString(); + const provider = new ethers.JsonRpcProvider(rpcUrlString); + const balance = await provider.getBalance(wallet.address); + const balanceEth = ethers.formatEther(balance); + + results.push({ + chainId, + success: true, + address: wallet.address, + balance: balanceEth, + balanceWei: balance.toString() + }); + + logger.info(`💰 Сеть ${chainId}: ${balanceEth} ETH`); + } catch (error) { + logger.error(`❌ Ошибка проверки баланса в сети ${chainId}:`, error); + results.push({ + chainId, + success: false, + error: error.message + }); + } + } + + return { + success: true, + results + }; + } catch (error) { + logger.error('❌ Ошибка проверки балансов:', error); + throw error; + } + } + + /** + * Закрывает соединения + */ + async close() { + await this.deployParamsService.close(); + } +} + +module.exports = UnifiedDeploymentService; diff --git a/backend/test/DLE.test.js b/backend/test/DLE.test.js deleted file mode 100644 index 20c2d1d..0000000 --- a/backend/test/DLE.test.js +++ /dev/null @@ -1,1209 +0,0 @@ -/** - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - * All rights reserved. - * - * This software is proprietary and confidential. - * Unauthorized copying, modification, or distribution is prohibited. - * - * For licensing inquiries: info@hb3-accelerator.com - * Website: https://hb3-accelerator.com - * GitHub: https://github.com/HB3-ACCELERATOR - */ - -const { expect } = require("chai"); -const { ethers } = require("hardhat"); - -describe("DLE Smart Contract", function () { - let DLE; - let dle; - let owner; - let partner1, partner2, partner3; - let addrs; - let treasuryAddr, timelockAddr, readerAddr; - - const SAMPLE_CONFIG = { - name: "Test DLE", - symbol: "TDLE", - location: "Test Location", - coordinates: "0,0", - jurisdiction: 1, - okvedCodes: ["62.01"], - kpp: 123456789, - quorumPercentage: 51, - initialPartners: [], // Will set in beforeEach - initialAmounts: [], - supportedChainIds: [1, 137] - }; - - beforeEach(async function () { - [owner, partner1, partner2, partner3, ...addrs] = await ethers.getSigners(); - - DLE = await ethers.getContractFactory("DLE"); - - SAMPLE_CONFIG.initialPartners = [partner1.address, partner2.address, partner3.address]; - SAMPLE_CONFIG.initialAmounts = [ethers.parseEther("100"), ethers.parseEther("100"), ethers.parseEther("100")]; - - dle = await DLE.deploy(SAMPLE_CONFIG, 1, owner.address); // currentChainId=1 - await dle.waitForDeployment(); - - // Используем EOAs как адреса модулей (ядро не проверяет код при инициализации) - treasuryAddr = addrs[0].address; - timelockAddr = addrs[1].address; - readerAddr = addrs[2].address; - - // Initialize base modules - await dle.initializeBaseModules(treasuryAddr, timelockAddr, readerAddr); - }); - - describe("Deployment and Initialization", function () { - it("Should set correct initial state", async function () { - const info = await dle.dleInfo(); - expect(info.name).to.equal(SAMPLE_CONFIG.name); - expect(info.symbol).to.equal(SAMPLE_CONFIG.symbol); - expect(await dle.quorumPercentage()).to.equal(SAMPLE_CONFIG.quorumPercentage); - expect(await dle.currentChainId()).to.equal(1); - expect(await dle.totalSupply()).to.equal(ethers.parseEther("300")); - }); - - it("Should distribute initial tokens and delegate", async function () { - expect(await dle.balanceOf(partner1.address)).to.equal(ethers.parseEther("100")); - expect(await dle.getVotes(partner1.address)).to.equal(ethers.parseEther("100")); - }); - - it("Should initialize modules correctly", async function () { - // Modules are already initialized in beforeEach - expect(await dle.modules(ethers.keccak256(ethers.toUtf8Bytes("TREASURY")))).to.equal(treasuryAddr); - expect(await dle.modulesInitialized()).to.be.true; - - // Cannot initialize twice - await expect(dle.initializeBaseModules(treasuryAddr, timelockAddr, readerAddr)) - .to.be.revertedWithCustomError(dle, "ErrProposalExecuted"); - }); - - it("Should prevent non-initializer from initializing modules", async function () { - // Create a new DLE instance for this test - const newDle = await DLE.deploy(SAMPLE_CONFIG, 1, owner.address); - await newDle.waitForDeployment(); - - await expect(newDle.connect(partner1).initializeBaseModules(treasuryAddr, timelockAddr, readerAddr)) - .to.be.revertedWithCustomError(newDle, "ErrOnlyInitializer"); - }); - - it("Should prevent initialization with zero addresses", async function () { - // Create a new DLE instance for this test - const newDle = await DLE.deploy(SAMPLE_CONFIG, 1, owner.address); - await newDle.waitForDeployment(); - - await expect(newDle.initializeBaseModules(ethers.ZeroAddress, timelockAddr, readerAddr)) - .to.be.revertedWithCustomError(newDle, "ErrZeroAddress"); - }); - }); - - describe("Proposals", function () { - it("Should create a proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - - expect(await dle.proposalCounter()).to.equal(1); - }); - - it("Should not allow non-holders to create proposals", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - await expect(dle.connect(addrs[0]).createProposal(description, duration, operation, governanceChainId, targetChains, 0)) - .to.be.revertedWithCustomError(dle, "ErrNotHolder"); - }); - - it("Should prevent creating proposal with too short duration", async function () { - const description = "Test Proposal"; - const duration = 59; // Less than minimum - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - await expect(dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0)) - .to.be.revertedWithCustomError(dle, "ErrTooShort"); - }); - - it("Should prevent creating proposal with too long duration", async function () { - const description = "Test Proposal"; - const duration = 30 * 24 * 60 * 60 + 1; // More than maximum - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - await expect(dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0)) - .to.be.revertedWithCustomError(dle, "ErrTooLong"); - }); - - it("Should prevent creating proposal with unsupported governance chain", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 999; // Unsupported chain - const targetChains = [1, 137]; - - await expect(dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0)) - .to.be.revertedWithCustomError(dle, "ErrBadChain"); - }); - - it("Should prevent creating proposal with unsupported target chain", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [999]; // Unsupported chain - - await expect(dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0)) - .to.be.revertedWithCustomError(dle, "ErrBadTarget"); - }); - }); - - describe("Voting System", function () { - it("Should allow token holders to vote", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await dle.connect(partner1).vote(proposalId, true); - const proposal = await dle.proposals(proposalId); - expect(proposal.forVotes).to.equal(ethers.parseEther("100")); - }); - - it("Should allow voting against proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await dle.connect(partner1).vote(proposalId, false); - const proposal = await dle.proposals(proposalId); - expect(proposal.againstVotes).to.equal(ethers.parseEther("100")); - }); - - it("Should prevent voting twice", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await dle.connect(partner1).vote(proposalId, true); - await expect(dle.connect(partner1).vote(proposalId, true)) - .to.be.revertedWithCustomError(dle, "ErrAlreadyVoted"); - }); - - it("Should prevent voting on non-existent proposal", async function () { - await expect(dle.connect(partner1).vote(999, true)) - .to.be.revertedWithCustomError(dle, "ErrProposalMissing"); - }); - - it("Should prevent voting after deadline", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Fast forward time - await ethers.provider.send("evm_increaseTime", [3601]); - await ethers.provider.send("evm_mine"); - - await expect(dle.connect(partner1).vote(proposalId, true)) - .to.be.revertedWithCustomError(dle, "ErrProposalEnded"); - }); - - it("Should prevent voting in wrong chain", async function () { - // Create DLE in different chain - const DLE2 = await ethers.getContractFactory("DLE"); - const cfg2 = { ...SAMPLE_CONFIG, supportedChainIds: [1, 2] }; - const dle2 = await DLE2.deploy(cfg2, 2, owner.address); - await dle2.waitForDeployment(); - - // Initialize modules - await dle2.initializeBaseModules(treasuryAddr, timelockAddr, readerAddr); - - const description = "Cross-chain proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [2]; - - await dle2.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - - await expect(dle2.connect(partner1).vote(0, true)) - .to.be.revertedWithCustomError(dle2, "ErrWrongChain"); - }); - - it("Should prevent voting without tokens", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await expect(dle.connect(addrs[0]).vote(proposalId, true)) - .to.be.revertedWithCustomError(dle, "ErrNoPower"); - }); - }); - - describe("Proposal Result Checking", function () { - it("Should return correct result for passed proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - }); - - it("Should return correct result for failed proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote against - await dle.connect(partner1).vote(proposalId, false); - await dle.connect(partner2).vote(proposalId, false); - - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.false; - expect(quorumReached).to.be.true; - }); - - it("Should return correct result for proposal without quorum", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with insufficient quorum - await dle.connect(partner1).vote(proposalId, true); - - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.false; - expect(quorumReached).to.be.false; - }); - - it("Should handle non-existent proposal", async function () { - await expect(dle.checkProposalResult(999)) - .to.be.revertedWithCustomError(dle, "ErrProposalMissing"); - }); - }); - - describe("Execution by signatures (EIP-712)", function () { - it("Should execute proposal in non-governance chain with quorum signatures", async function () { - // Deploy DLE with currentChainId=2 and support [1,2] - const DLE2 = await ethers.getContractFactory("DLE"); - const cfg2 = { - name: SAMPLE_CONFIG.name, - symbol: SAMPLE_CONFIG.symbol, - location: SAMPLE_CONFIG.location, - coordinates: SAMPLE_CONFIG.coordinates, - jurisdiction: SAMPLE_CONFIG.jurisdiction, - okvedCodes: SAMPLE_CONFIG.okvedCodes, - kpp: SAMPLE_CONFIG.kpp, - quorumPercentage: 51, - initialPartners: [partner1.address, partner2.address, partner3.address], - initialAmounts: [ethers.parseEther("100"), ethers.parseEther("100"), ethers.parseEther("100")], - supportedChainIds: [1, 2] - }; - const dle2 = await DLE2.deploy(cfg2, 2, owner.address); - await dle2.waitForDeployment(); - - // Initialize modules - await dle2.initializeBaseModules(treasuryAddr, timelockAddr, readerAddr); - - // Create proposal with governanceChainId=1 and targetChains=[2] - // Use a valid operation - add a module - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TEST_MODULE")); - const moduleAddress = addrs[5].address; - - const tx = await dle2.connect(partner1).createAddModuleProposal("Sig Exec", 3600, moduleId, moduleAddress, 1); - const receipt = await tx.wait(); - const proposalId = (await dle2.proposalCounter()) - 1n; - - // Get snapshotTimepoint - const p = await dle2.proposals(proposalId); - const snapshotTimepoint = p.snapshotTimepoint; - - // Create EIP-712 signatures for three partners - const domain = { - name: cfg2.name, - version: "1", - chainId: (await ethers.provider.getNetwork()).chainId, - verifyingContract: await dle2.getAddress() - }; - - const types = { - ExecutionApproval: [ - { name: "proposalId", type: "uint256" }, - { name: "operationHash", type: "bytes32" }, - { name: "chainId", type: "uint256" }, - { name: "snapshotTimepoint", type: "uint256" } - ] - }; - - const value = { - proposalId: proposalId, - operationHash: ethers.keccak256(p.operation), - chainId: 2, - snapshotTimepoint - }; - - const sig1 = await partner1.signTypedData(domain, types, value); - const sig2 = await partner2.signTypedData(domain, types, value); - const sig3 = await partner3.signTypedData(domain, types, value); - - // Execute by signatures (quorum 51%, total 300 -> need 153, we have 300) - await expect(dle2.executeProposalBySignatures(proposalId, [partner1.address, partner2.address, partner3.address], [sig1, sig2, sig3])) - .to.emit(dle2, "ProposalExecuted"); - }); - }); - - describe("Token Transfer Blocking", function () { - it("Should revert direct transfers", async function () { - await expect(dle.connect(partner1).transfer(partner2.address, ethers.parseEther("10"))) - .to.be.revertedWithCustomError(dle, "ErrTransfersDisabled"); - }); - - it("Should revert approvals", async function () { - await expect(dle.connect(partner1).approve(partner2.address, ethers.parseEther("10"))) - .to.be.revertedWithCustomError(dle, "ErrApprovalsDisabled"); - }); - - it("Should revert transferFrom", async function () { - await expect(dle.connect(partner2).transferFrom(partner1.address, partner3.address, ethers.parseEther("10"))) - .to.be.revertedWithCustomError(dle, "ErrTransfersDisabled"); - }); - }); - - describe("Chain Management", function () { - it("Should return correct chain information", async function () { - expect(await dle.getSupportedChainCount()).to.equal(2); - expect(await dle.getSupportedChainId(0)).to.equal(1); - expect(await dle.getSupportedChainId(1)).to.equal(137); - expect(await dle.getCurrentChainId()).to.equal(1); - }); - }); - - describe("View Functions", function () { - it("Should return correct DLE info", async function () { - const info = await dle.dleInfo(); - expect(info.name).to.equal(SAMPLE_CONFIG.name); - expect(info.symbol).to.equal(SAMPLE_CONFIG.symbol); - expect(info.location).to.equal(SAMPLE_CONFIG.location); - expect(info.coordinates).to.equal(SAMPLE_CONFIG.coordinates); - expect(info.jurisdiction).to.equal(SAMPLE_CONFIG.jurisdiction); - expect(info.kpp).to.equal(SAMPLE_CONFIG.kpp); - expect(info.isActive).to.be.true; - }); - - it("Should return correct voting power", async function () { - expect(await dle.getVotes(partner1.address)).to.equal(ethers.parseEther("100")); - }); - - it("Should return correct nonces", async function () { - expect(await dle.nonces(partner1.address)).to.equal(0); - }); - }); - - describe("Delegation", function () { - it("Should allow self-delegation", async function () { - await dle.connect(partner1).delegate(partner1.address); - expect(await dle.getVotes(partner1.address)).to.equal(ethers.parseEther("100")); - }); - - it("Should prevent delegation to others", async function () { - await expect(dle.connect(partner1).delegate(partner2.address)) - .to.be.revertedWith("Delegation disabled"); - }); - }); - - describe("Error Handling", function () { - it("Should handle invalid operations", async function () { - await expect(dle.connect(partner1).transfer(partner2.address, ethers.parseEther("10"))) - .to.be.revertedWithCustomError(dle, "ErrTransfersDisabled"); - }); - - it("Should handle unsupported operations", async function () { - await expect(dle.connect(partner1).approve(partner2.address, ethers.parseEther("10"))) - .to.be.revertedWithCustomError(dle, "ErrApprovalsDisabled"); - }); - }); - - describe("Proposal Cancellation", function () { - it("Should allow canceling proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await dle.connect(partner1).cancelProposal(proposalId, "Test cancellation"); - const proposal = await dle.proposals(proposalId); - expect(proposal.canceled).to.be.true; - }); - - it("Should prevent voting on canceled proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await dle.connect(partner1).cancelProposal(proposalId, "Test cancellation"); - - await expect(dle.connect(partner1).vote(proposalId, true)) - .to.be.revertedWithCustomError(dle, "ErrProposalCanceled"); - }); - - it("Should prevent executing canceled proposal", async function () { - const description = "Test Proposal"; - const duration = 3600; - const operation = ethers.toUtf8Bytes("test operation"); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - await dle.connect(partner1).cancelProposal(proposalId, "Test cancellation"); - - await expect(dle.executeProposal(proposalId)) - .to.be.revertedWithCustomError(dle, "ErrProposalCanceled"); - }); - }); - - describe("Logo URI Management", function () { - it("Should allow initializer to set logo URI", async function () { - const logoURI = "https://example.com/logo.png"; - await dle.initializeLogoURI(logoURI); - expect(await dle.logoURI()).to.equal(logoURI); - }); - - it("Should prevent setting logo URI twice", async function () { - const logoURI = "https://example.com/logo.png"; - await dle.initializeLogoURI(logoURI); - await expect(dle.initializeLogoURI(logoURI)) - .to.be.revertedWithCustomError(dle, "ErrLogoAlreadySet"); - }); - - it("Should prevent non-initializer from setting logo", async function () { - const logoURI = "https://example.com/logo.png"; - await expect(dle.connect(partner1).initializeLogoURI(logoURI)) - .to.be.revertedWithCustomError(dle, "ErrOnlyInitializer"); - }); - }); - - describe("Module Management", function () { - it("Should add module through governance", async function () { - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TEST_MODULE")); - const moduleAddress = addrs[5].address; - - // Create proposal to add module - const description = "Add test module"; - const duration = 3600; - const chainId = 1; - - const tx = await dle.connect(partner1).createAddModuleProposal(description, duration, moduleId, moduleAddress, chainId); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - // Check proposal result - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - - // Execute proposal - await dle.executeProposal(proposalId); - - // Verify module was added - expect(await dle.activeModules(moduleId)).to.be.true; - expect(await dle.modules(moduleId)).to.equal(moduleAddress); - }); - - it("Should remove module through governance", async function () { - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TEST_MODULE_FOR_REMOVAL")); - const moduleAddress = addrs[6].address; - - // First add the module - const addDescription = "Add test module for removal"; - const duration = 3600; - const chainId = 1; - - const addTx = await dle.connect(partner1).createAddModuleProposal(addDescription, duration, moduleId, moduleAddress, chainId); - const addReceipt = await addTx.wait(); - const addProposalId = (await dle.proposalCounter()) - 1n; - - // Vote and execute add proposal - await dle.connect(partner1).vote(addProposalId, true); - await dle.connect(partner2).vote(addProposalId, true); - - // Check proposal result - const [addPassed, addQuorumReached] = await dle.checkProposalResult(addProposalId); - expect(addPassed).to.be.true; - expect(addQuorumReached).to.be.true; - - await dle.executeProposal(addProposalId); - - // Now create proposal to remove module - const removeDescription = "Remove test module"; - const removeTx = await dle.connect(partner1).createRemoveModuleProposal(removeDescription, duration, moduleId, chainId); - const removeReceipt = await removeTx.wait(); - const removeProposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(removeProposalId, true); - await dle.connect(partner2).vote(removeProposalId, true); - - // Check proposal result - const [removePassed, removeQuorumReached] = await dle.checkProposalResult(removeProposalId); - expect(removePassed).to.be.true; - expect(removeQuorumReached).to.be.true; - - // Execute proposal - await dle.executeProposal(removeProposalId); - - // Verify module was removed - expect(await dle.activeModules(moduleId)).to.be.false; - expect(await dle.modules(moduleId)).to.equal(ethers.ZeroAddress); - }); - }); - - describe("Chain Management", function () { - it("Should add supported chain through governance", async function () { - const newChainId = 56; // BSC - - // Create proposal to add chain using internal operation - const description = "Add BSC chain"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _addSupportedChain(uint256 chainId)" - ]); - const operation = dleInterface.encodeFunctionData("_addSupportedChain", [newChainId]); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - // Check proposal result - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - - // Execute proposal - await dle.executeProposal(proposalId); - - // Verify chain was added - expect(await dle.supportedChains(newChainId)).to.be.true; - }); - - it("Should remove supported chain through governance", async function () { - const chainToRemove = 137; // Polygon - - // Create proposal to remove chain using internal operation - const description = "Remove Polygon chain"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _removeSupportedChain(uint256 chainId)" - ]); - const operation = dleInterface.encodeFunctionData("_removeSupportedChain", [chainToRemove]); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - // Check proposal result - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - - // Execute proposal - await dle.executeProposal(proposalId); - - // Verify chain was removed - expect(await dle.supportedChains(chainToRemove)).to.be.false; - }); - - it("Should prevent removing current chain", async function () { - const currentChainId = 1; - - // Create proposal to remove current chain using internal operation - const description = "Remove current chain"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _removeSupportedChain(uint256 chainId)" - ]); - const operation = dleInterface.encodeFunctionData("_removeSupportedChain", [currentChainId]); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - // Check proposal result - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - - // Execute proposal should fail - await expect(dle.executeProposal(proposalId)) - .to.be.revertedWith("Cannot remove current chain"); - }); - }); - - describe("DLE Information Management", function () { - it("Should update quorum percentage through governance", async function () { - const newQuorum = 60; - - // Create proposal to update quorum using internal operation - const description = "Update quorum percentage"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [newQuorum]); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - // Check proposal result - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - - // Execute proposal - await dle.executeProposal(proposalId); - - // Verify quorum was updated - expect(await dle.quorumPercentage()).to.equal(newQuorum); - }); - - it("Should update voting durations through governance", async function () { - const newMinDuration = 7200; // 2 hours - const newMaxDuration = 14 * 24 * 60 * 60; // 14 days - - // Create proposal to update durations using internal operation - const description = "Update voting durations"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateVotingDurations(uint256 minDuration, uint256 maxDuration)" - ]); - const operation = dleInterface.encodeFunctionData("_updateVotingDurations", [newMinDuration, newMaxDuration]); - const governanceChainId = 1; - const targetChains = [1, 137]; - - const tx = await dle.connect(partner1).createProposal(description, duration, operation, governanceChainId, targetChains, 0); - const receipt = await tx.wait(); - const proposalId = (await dle.proposalCounter()) - 1n; - - // Vote with quorum - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - - // Check proposal result - const [passed, quorumReached] = await dle.checkProposalResult(proposalId); - expect(passed).to.be.true; - expect(quorumReached).to.be.true; - - // Execute proposal - await dle.executeProposal(proposalId); - - // Verify durations were updated - expect(await dle.minVotingDuration()).to.equal(newMinDuration); - expect(await dle.maxVotingDuration()).to.equal(newMaxDuration); - }); - }); - - describe("Token Management", function () { - it("Should prevent insufficient balance transfer", async function () { - await expect(dle.connect(partner1).transfer(partner2.address, ethers.parseEther("1000"))) - .to.be.revertedWithCustomError(dle, "ErrTransfersDisabled"); - }); - }); - - describe("Proposal State Management", function () { - it("Should handle non-existent proposal state", async function () { - await expect(dle.getProposalState(999)) - .to.be.revertedWith("Proposal does not exist"); - }); - - it("Should return DLE info", async function () { - const info = await dle.dleInfo(); - expect(info.name).to.equal(SAMPLE_CONFIG.name); - expect(info.symbol).to.equal(SAMPLE_CONFIG.symbol); - expect(info.location).to.equal(SAMPLE_CONFIG.location); - expect(info.coordinates).to.equal(SAMPLE_CONFIG.coordinates); - expect(info.jurisdiction).to.equal(SAMPLE_CONFIG.jurisdiction); - expect(info.kpp).to.equal(SAMPLE_CONFIG.kpp); - expect(info.isActive).to.be.true; - }); - - it("Should check if module is active", async function () { - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("NON_EXISTENT")); - expect(await dle.activeModules(moduleId)).to.be.false; - }); - - it("Should return module address", async function () { - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("NON_EXISTENT")); - expect(await dle.modules(moduleId)).to.equal(ethers.ZeroAddress); - }); - - it("Should check if chain is supported", async function () { - expect(await dle.supportedChains(1)).to.be.true; - expect(await dle.supportedChains(137)).to.be.true; - expect(await dle.supportedChains(999)).to.be.false; - }); - - it("Should return current chain ID", async function () { - expect(await dle.currentChainId()).to.equal(1); - }); - - it("Should check if DLE is active", async function () { - const info = await dle.dleInfo(); - expect(info.isActive).to.be.true; - }); - - it("Should prevent adding zero address module", async function () { - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("TEST_MODULE")); - - // Create proposal to add zero address module - const description = "Add Zero Address Module"; - const duration = 3600; - const chainId = 1; - - await expect(dle.connect(partner1).createAddModuleProposal(description, duration, moduleId, ethers.ZeroAddress, chainId)) - .to.be.revertedWithCustomError(dle, "ErrZeroAddress"); - }); - - it("Should prevent adding duplicate module", async function () { - // Test that we can't add modules that already exist - const treasuryId = ethers.keccak256(ethers.toUtf8Bytes("TREASURY")); - const newTreasuryAddress = addrs[6].address; - - // Try to add TREASURY module again (it already exists) - await expect(dle.connect(partner1).createAddModuleProposal("Add Duplicate Treasury", 3600, treasuryId, newTreasuryAddress, 1)) - .to.be.revertedWithCustomError(dle, "ErrProposalExecuted"); - }); - - it("Should prevent removing non-existent module", async function () { - const moduleId = ethers.keccak256(ethers.toUtf8Bytes("NONEXISTENT_MODULE")); - - // Create a proposal to remove non-existent module should revert immediately - await expect(dle.connect(partner1).createRemoveModuleProposal( - "Remove non-existent module", - 2 * 24 * 60 * 60, // 2 days - moduleId, - 1 // chainId - )).to.be.revertedWithCustomError(dle, "ErrProposalMissing"); - }); - }); - - describe("Additional Coverage Tests", function () { - it("Should test getProposalState for all states", async function () { - // Создаем предложение - const description = "Test proposal for state coverage"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1], 0); - const proposalId = 0; - - // State 0: Pending (до голосования) - expect(await dle.getProposalState(proposalId)).to.equal(0); - - // State 5: ReadyForExecution (прошло голосование, достигнут кворум) - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - await dle.connect(partner3).vote(proposalId, true); - expect(await dle.getProposalState(proposalId)).to.equal(5); - - // State 3: Executed (исполнено) - await ethers.provider.send("evm_increaseTime", [3601]); - await ethers.provider.send("evm_mine"); - await dle.executeProposal(proposalId); - expect(await dle.getProposalState(proposalId)).to.equal(3); - }); - - it("Should test getProposalState for defeated proposal", async function () { - // Создаем предложение - const description = "Test proposal for defeat"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1], 0); - const proposalId = 0; - - // Голосуем против - await dle.connect(partner1).vote(proposalId, false); - await dle.connect(partner2).vote(proposalId, false); - await dle.connect(partner3).vote(proposalId, false); - - // State 2: Defeated (прошло время голосования и не прошло) - await ethers.provider.send("evm_increaseTime", [3601]); - await ethers.provider.send("evm_mine"); - expect(await dle.getProposalState(proposalId)).to.equal(2); - }); - - it("Should test getProposalState for canceled proposal", async function () { - // Создаем предложение - const description = "Test proposal for cancellation"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1], 0); - const proposalId = 0; - - // State 4: Canceled - await dle.connect(partner1).cancelProposal(proposalId, "Test cancellation"); - expect(await dle.getProposalState(proposalId)).to.equal(4); - }); - - it("Should test getProposalState for ready for execution", async function () { - // Создаем предложение - const description = "Test proposal for ready state"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1], 0); - const proposalId = 0; - - // Голосуем за - await dle.connect(partner1).vote(proposalId, true); - await dle.connect(partner2).vote(proposalId, true); - await dle.connect(partner3).vote(proposalId, true); - - // State 5: ReadyForExecution (прошло голосование, достигнут кворум, но еще не исполнено) - expect(await dle.getProposalState(proposalId)).to.equal(5); - }); - - it("Should test _isTargetChain function", async function () { - // Создаем предложение с несколькими целевыми цепочками - const description = "Test proposal with multiple target chains"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1, 137], 0); - const proposalId = 0; - - // Проверяем что предложение создано с правильными целевыми цепочками - // Используем getProposalState для проверки что предложение создано корректно - expect(await dle.getProposalState(proposalId)).to.equal(0); - - // Проверяем что предложение существует и можно голосовать - await dle.connect(partner1).vote(proposalId, true); - expect(await dle.getProposalState(proposalId)).to.equal(0); // все еще pending - }); - - it("Should test _update function override", async function () { - // Тестируем делегирование (которое вызывает _update) - await dle.connect(partner1).delegate(partner1.address); - expect(await dle.delegates(partner1.address)).to.equal(partner1.address); - }); - - it("Should test nonces function override", async function () { - // Тестируем nonces - const nonce = await dle.nonces(partner1.address); - expect(nonce).to.be.a("bigint"); - }); - - it("Should test _delegate function override", async function () { - // Тестируем делегирование самому себе (разрешено) - await dle.connect(partner1).delegate(partner1.address); - expect(await dle.delegates(partner1.address)).to.equal(partner1.address); - - // Тестируем делегирование другому (запрещено) - await expect(dle.connect(partner1).delegate(partner2.address)) - .to.be.revertedWith("Delegation disabled"); - }); - - it("Should test isActive function", async function () { - // По умолчанию DLE активен - expect(await dle.isActive()).to.be.true; - }); - - it("Should test getModuleAddress for non-existent module", async function () { - // Тестируем getModuleAddress для несуществующего модуля - const nonExistentModuleId = ethers.id("NON_EXISTENT"); - const address = await dle.getModuleAddress(nonExistentModuleId); - expect(address).to.equal(ethers.ZeroAddress); - }); - - it("Should test isChainSupported for non-supported chain", async function () { - // Тестируем isChainSupported для неподдерживаемой цепочки - expect(await dle.isChainSupported(999)).to.be.false; - }); - - it("Should test getCurrentChainId", async function () { - // Тестируем getCurrentChainId - const currentChainId = await dle.getCurrentChainId(); - expect(currentChainId).to.equal(1); // По умолчанию 1 - }); - - it("Should test getDLEInfo", async function () { - // Тестируем getDLEInfo - const dleInfo = await dle.getDLEInfo(); - expect(dleInfo.name).to.equal("Test DLE"); - expect(dleInfo.symbol).to.equal("TDLE"); - expect(dleInfo.location).to.equal("Test Location"); - expect(dleInfo.coordinates).to.equal("0,0"); - expect(dleInfo.jurisdiction).to.equal(1); - expect(dleInfo.okvedCodes).to.deep.equal(["62.01"]); - expect(dleInfo.kpp).to.equal(123456789); // Исправляем ожидаемое значение - expect(dleInfo.isActive).to.be.true; - }); - - it("Should test isModuleActive for existing module", async function () { - // Тестируем isModuleActive для существующего модуля - const treasuryId = ethers.id("TREASURY"); - expect(await dle.isModuleActive(treasuryId)).to.be.true; - }); - - it("Should test isModuleActive for non-existent module", async function () { - // Тестируем isModuleActive для несуществующего модуля - const nonExistentModuleId = ethers.id("NON_EXISTENT"); - expect(await dle.isModuleActive(nonExistentModuleId)).to.be.false; - }); - - it("Should test getProposalState for non-existent proposal", async function () { - // Тестируем getProposalState для несуществующего предложения - await expect(dle.getProposalState(999)) - .to.be.revertedWith("Proposal does not exist"); - }); - - it("Should test getProposalState for defeated after deadline", async function () { - // Создаем предложение - const description = "Test proposal for defeated after deadline"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1], 0); - const proposalId = 0; - - // Голосуем против - await dle.connect(partner1).vote(proposalId, false); - await dle.connect(partner2).vote(proposalId, false); - // partner3 не голосует - - // Проходит время голосования - await ethers.provider.send("evm_increaseTime", [3601]); - await ethers.provider.send("evm_mine"); - - // State 2: Defeated (прошло время голосования и не прошло) - expect(await dle.getProposalState(proposalId)).to.equal(2); - }); - - it("Should test getProposalState for pending during voting", async function () { - // Создаем предложение - const description = "Test proposal for pending during voting"; - const duration = 3600; - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - await dle.connect(partner1).createProposal(description, duration, operation, 1, [1], 0); - const proposalId = 0; - - // Голосуем только один раз - await dle.connect(partner1).vote(proposalId, true); - - // State 0: Pending (голосование еще идет, не достигнут кворум) - expect(await dle.getProposalState(proposalId)).to.equal(0); - }); - - it("Should test getProposalState for pending with mixed votes", async function () { - // Create a proposal using partner1 who has tokens - const tx = await dle.connect(partner1).createProposal( - "Test proposal with mixed votes", - 3600, - "0x12345678", - 1, - [1], // Only supported chain - partner1.address - ); - const receipt = await tx.wait(); - const event = receipt.logs.find(log => log.fragment && log.fragment.name === "ProposalCreated"); - const proposalId = event.args.proposalId; - - // Vote for the proposal - await dle.connect(partner1).vote(proposalId, true); - - // Vote against the proposal - await dle.connect(partner2).vote(proposalId, false); - - // Check proposal state (should be pending) - const state = await dle.getProposalState(proposalId); - expect(state).to.equal(0); // Pending - }); - - it("Should test blocked transfer function", async function () { - // Try to transfer tokens (should be blocked) - await expect( - dle.connect(addrs[0]).transfer(addrs[1].address, ethers.parseEther("100")) - ).to.be.revertedWithCustomError(dle, "ErrTransfersDisabled"); - }); - - it("Should test blocked transferFrom function", async function () { - // Try to transfer tokens via transferFrom (should be blocked) - await expect( - dle.connect(addrs[0]).transferFrom(addrs[0].address, addrs[1].address, ethers.parseEther("100")) - ).to.be.revertedWithCustomError(dle, "ErrTransfersDisabled"); - }); - - it("Should test blocked approve function", async function () { - // Try to approve tokens (should be blocked) - await expect( - dle.connect(addrs[0]).approve(addrs[1].address, ethers.parseEther("100")) - ).to.be.revertedWithCustomError(dle, "ErrApprovalsDisabled"); - }); - - it("Should test token transfer through governance", async function () { - // Create a proposal to transfer tokens using a simple operation - const dleInterface = new ethers.Interface([ - "function _updateQuorumPercentage(uint256 newQuorumPercentage)" - ]); - const operation = dleInterface.encodeFunctionData("_updateQuorumPercentage", [60]); - - const tx = await dle.connect(partner1).createProposal( - "Update quorum percentage", - 3600, - operation, - 1, - [1], - partner1.address - ); - const receipt = await tx.wait(); - const event = receipt.logs.find(log => log.fragment && log.fragment.name === "ProposalCreated"); - const proposalId = event.args.proposalId; - - // Vote for the proposal - await dle.connect(partner1).vote(proposalId, true); - - // Vote for the proposal with partner2 to reach quorum - await dle.connect(partner2).vote(proposalId, true); - - // Advance time to pass deadline - await ethers.provider.send("evm_increaseTime", [3601]); - await ethers.provider.send("evm_mine"); - - // Execute the proposal - await dle.connect(partner1).executeProposal(proposalId); - - // Check that quorum was updated - const quorumPercentage = await dle.quorumPercentage(); - expect(quorumPercentage).to.equal(60); - }); - }); -}); \ No newline at end of file diff --git a/backend/utils/NonceManager.js b/backend/utils/NonceManager.js deleted file mode 100644 index 6b7ff02..0000000 --- a/backend/utils/NonceManager.js +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Copyright (c) 2024-2025 Тарабанов Александр Викторович - * All rights reserved. - * - * This software is proprietary and confidential. - * Unauthorized copying, modification, or distribution is prohibited. - * - * For licensing inquiries: info@hb3-accelerator.com - * Website: https://hb3-accelerator.com - * GitHub: https://github.com/HB3-ACCELERATOR - */ - -const { ethers } = require('ethers'); - -/** - * Менеджер nonce для синхронизации транзакций в мультичейн-деплое - * Обеспечивает правильную последовательность транзакций без конфликтов - */ -class NonceManager { - constructor() { - this.nonceCache = new Map(); // Кэш nonce для каждого кошелька - this.pendingTransactions = new Map(); // Ожидающие транзакции - this.locks = new Map(); // Блокировки для предотвращения конкурентного доступа - } - - /** - * Получить актуальный nonce для кошелька в сети - * @param {string} rpcUrl - URL RPC провайдера - * @param {string} walletAddress - Адрес кошелька - * @param {boolean} usePending - Использовать pending транзакции - * @returns {Promise} Актуальный nonce - */ - async getCurrentNonce(rpcUrl, walletAddress, usePending = true) { - const key = `${walletAddress}-${rpcUrl}`; - - try { - // Создаем провайдер из rpcUrl - const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true }); - - const nonce = await Promise.race([ - provider.getTransactionCount(walletAddress, usePending ? 'pending' : 'latest'), - new Promise((_, reject) => setTimeout(() => reject(new Error('Nonce timeout')), 30000)) - ]); - - console.log(`[NonceManager] Получен nonce для ${walletAddress} в сети ${rpcUrl}: ${nonce}`); - return nonce; - } catch (error) { - console.error(`[NonceManager] Ошибка получения nonce для ${walletAddress}:`, error.message); - - // Если сеть недоступна, возвращаем 0 как fallback - if (error.message.includes('network is not available') || error.message.includes('NETWORK_ERROR')) { - console.warn(`[NonceManager] Сеть недоступна, используем nonce 0 для ${walletAddress}`); - return 0; - } - - throw error; - } - } - - /** - * Заблокировать nonce для транзакции - * @param {ethers.Wallet} wallet - Кошелек - * @param {ethers.Provider} provider - Провайдер сети - * @returns {Promise} Заблокированный nonce - */ - async lockNonce(rpcUrl, walletAddress) { - const key = `${walletAddress}-${rpcUrl}`; - - // Ждем освобождения блокировки - while (this.locks.has(key)) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - - // Устанавливаем блокировку - this.locks.set(key, true); - - try { - const currentNonce = await this.getCurrentNonce(rpcUrl, walletAddress); - const lockedNonce = currentNonce; - - // Обновляем кэш - this.nonceCache.set(key, lockedNonce + 1); - - console.log(`[NonceManager] Заблокирован nonce ${lockedNonce} для ${walletAddress} в сети ${rpcUrl}`); - return lockedNonce; - } finally { - // Освобождаем блокировку - this.locks.delete(key); - } - } - - /** - * Освободить nonce после успешной транзакции - * @param {ethers.Wallet} wallet - Кошелек - * @param {ethers.Provider} provider - Провайдер сети - * @param {number} nonce - Использованный nonce - */ - releaseNonce(rpcUrl, walletAddress, nonce) { - const key = `${walletAddress}-${rpcUrl}`; - const cachedNonce = this.nonceCache.get(key) || 0; - - if (nonce >= cachedNonce) { - this.nonceCache.set(key, nonce + 1); - } - - console.log(`[NonceManager] Освобожден nonce ${nonce} для ${walletAddress} в сети ${rpcUrl}`); - } - - /** - * Синхронизировать nonce между сетями - * @param {Array} networks - Массив сетей с кошельками - * @returns {Promise} Синхронизированный nonce - */ - async synchronizeNonce(networks) { - console.log(`[NonceManager] Начинаем синхронизацию nonce для ${networks.length} сетей`); - - // Получаем nonce для всех сетей - const nonces = await Promise.all( - networks.map(async (network, index) => { - try { - const nonce = await this.getCurrentNonce(network.rpcUrl, network.wallet.address); - console.log(`[NonceManager] Сеть ${index + 1}/${networks.length} (${network.chainId}): nonce=${nonce}`); - return { chainId: network.chainId, nonce, index }; - } catch (error) { - console.error(`[NonceManager] Ошибка получения nonce для сети ${network.chainId}:`, error.message); - throw error; - } - }) - ); - - // Находим максимальный nonce - const maxNonce = Math.max(...nonces.map(n => n.nonce)); - console.log(`[NonceManager] Максимальный nonce: ${maxNonce}`); - - // Выравниваем nonce во всех сетях - for (const network of networks) { - const currentNonce = nonces.find(n => n.chainId === network.chainId)?.nonce || 0; - - if (currentNonce < maxNonce) { - console.log(`[NonceManager] Выравниваем nonce в сети ${network.chainId} с ${currentNonce} до ${maxNonce}`); - await this.alignNonce(network.wallet, network.provider, currentNonce, maxNonce); - } - } - - console.log(`[NonceManager] Синхронизация nonce завершена. Целевой nonce: ${maxNonce}`); - return maxNonce; - } - - /** - * Выровнять nonce до целевого значения - * @param {ethers.Wallet} wallet - Кошелек - * @param {ethers.Provider} provider - Провайдер сети - * @param {number} currentNonce - Текущий nonce - * @param {number} targetNonce - Целевой nonce - */ - async alignNonce(wallet, provider, currentNonce, targetNonce) { - const burnAddress = "0x000000000000000000000000000000000000dEaD"; - let nonce = currentNonce; - - while (nonce < targetNonce) { - try { - // Получаем актуальный nonce перед каждой транзакцией - const actualNonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address); - if (actualNonce > nonce) { - nonce = actualNonce; - continue; - } - - const feeOverrides = await this.getFeeOverrides(provider); - const txReq = { - to: burnAddress, - value: 0n, - nonce: nonce, - gasLimit: 21000, - ...feeOverrides - }; - - console.log(`[NonceManager] Отправляем заполняющую транзакцию nonce=${nonce} в сети ${provider._network?.chainId}`); - const tx = await wallet.sendTransaction(txReq); - await tx.wait(); - - console.log(`[NonceManager] Заполняющая транзакция nonce=${nonce} подтверждена в сети ${provider._network?.chainId}`); - nonce++; - - // Небольшая задержка между транзакциями - await new Promise(resolve => setTimeout(resolve, 1000)); - - } catch (error) { - console.error(`[NonceManager] Ошибка заполняющей транзакции nonce=${nonce}:`, error.message); - - if (error.message.includes('nonce too low')) { - // Обновляем nonce и пробуем снова - nonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address); - continue; - } - - throw error; - } - } - } - - /** - * Получить параметры комиссии для сети - * @param {ethers.Provider} provider - Провайдер сети - * @returns {Promise} Параметры комиссии - */ - async getFeeOverrides(provider) { - try { - const feeData = await provider.getFeeData(); - - if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) { - return { - maxFeePerGas: feeData.maxFeePerGas, - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas - }; - } else { - return { - gasPrice: feeData.gasPrice - }; - } - } catch (error) { - console.warn(`[NonceManager] Ошибка получения fee data:`, error.message); - return {}; - } - } - - /** - * Безопасная отправка транзакции с правильным nonce - * @param {ethers.Wallet} wallet - Кошелек - * @param {ethers.Provider} provider - Провайдер сети - * @param {Object} txData - Данные транзакции - * @param {number} maxRetries - Максимальное количество попыток - * @returns {Promise} Результат транзакции - */ - async sendTransactionSafely(wallet, provider, txData, maxRetries = 1) { - const rpcUrl = provider._getConnection().url; - const walletAddress = wallet.address; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - // Получаем актуальный nonce - const nonce = await this.lockNonce(rpcUrl, walletAddress); - - const tx = await wallet.sendTransaction({ - ...txData, - nonce: nonce - }); - - console.log(`[NonceManager] Транзакция отправлена с nonce=${nonce} в сети ${provider._network?.chainId}`); - - // Ждем подтверждения - await tx.wait(); - - // Освобождаем nonce - this.releaseNonce(rpcUrl, walletAddress, nonce); - - return tx; - - } catch (error) { - console.error(`[NonceManager] Попытка ${attempt + 1}/${maxRetries} неудачна:`, error.message); - - if (error.message.includes('nonce too low') && attempt < maxRetries - 1) { - // Обновляем nonce и пробуем снова - await new Promise(resolve => setTimeout(resolve, 2000)); - continue; - } - - if (attempt === maxRetries - 1) { - throw error; - } - } - } - } - - /** - * Очистить кэш nonce - */ - clearCache() { - this.nonceCache.clear(); - this.pendingTransactions.clear(); - this.locks.clear(); - console.log(`[NonceManager] Кэш nonce очищен`); - } -} - -module.exports = NonceManager; diff --git a/backend/utils/apiKeyManager.js b/backend/utils/apiKeyManager.js new file mode 100644 index 0000000..8dff04e --- /dev/null +++ b/backend/utils/apiKeyManager.js @@ -0,0 +1,107 @@ +/** + * Централизованный менеджер API ключей + * Унифицирует работу с API ключами Etherscan + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const logger = require('./logger'); + +class ApiKeyManager { + /** + * Получает API ключ Etherscan из различных источников + * @param {Object} params - Параметры деплоя + * @param {Object} reqBody - Тело запроса (опционально) + * @returns {string|null} - API ключ или null + */ + static getEtherscanApiKey(params = {}, reqBody = {}) { + // Приоритет источников: + // 1. Из параметров деплоя (БД) + // 2. Из тела запроса (фронтенд) + // 3. Из переменных окружения + // 4. Из секретов + + let apiKey = null; + + // 1. Из параметров деплоя (БД) - приоритет 1 + if (params.etherscan_api_key) { + apiKey = params.etherscan_api_key; + logger.info('[API_KEY] ✅ Ключ получен из параметров деплоя (БД)'); + } + + // 2. Из тела запроса (фронтенд) - приоритет 2 + else if (reqBody.etherscanApiKey) { + apiKey = reqBody.etherscanApiKey; + logger.info('[API_KEY] ✅ Ключ получен из тела запроса (фронтенд)'); + } + + // 3. Из переменных окружения - приоритет 3 + else if (process.env.ETHERSCAN_API_KEY) { + apiKey = process.env.ETHERSCAN_API_KEY; + logger.info('[API_KEY] ✅ Ключ получен из переменных окружения'); + } + + // 4. Из секретов - приоритет 4 + else if (process.env.ETHERSCAN_V2_API_KEY) { + apiKey = process.env.ETHERSCAN_V2_API_KEY; + logger.info('[API_KEY] ✅ Ключ получен из секретов'); + } + + if (apiKey) { + logger.info(`[API_KEY] 🔑 API ключ найден: ${apiKey.substring(0, 8)}...`); + return apiKey; + } else { + logger.warn('[API_KEY] ⚠️ API ключ Etherscan не найден'); + return null; + } + } + + /** + * Устанавливает API ключ в переменные окружения + * @param {string} apiKey - API ключ + */ + static setEtherscanApiKey(apiKey) { + if (apiKey) { + process.env.ETHERSCAN_API_KEY = apiKey; + logger.info(`[API_KEY] 🔧 API ключ установлен в переменные окружения: ${apiKey.substring(0, 8)}...`); + } + } + + /** + * Проверяет наличие API ключа + * @param {string} apiKey - API ключ для проверки + * @returns {boolean} - true если ключ валидный + */ + static validateApiKey(apiKey) { + if (!apiKey || typeof apiKey !== 'string') { + logger.warn('[API_KEY] ❌ API ключ не валидный: пустой или не строка'); + return false; + } + + if (apiKey.length < 10) { + logger.warn('[API_KEY] ❌ API ключ слишком короткий'); + return false; + } + + logger.info('[API_KEY] ✅ API ключ валидный'); + return true; + } + + /** + * Получает и устанавливает API ключ (универсальный метод) + * @param {Object} params - Параметры деплоя + * @param {Object} reqBody - Тело запроса (опционально) + * @returns {string|null} - API ключ или null + */ + static getAndSetEtherscanApiKey(params = {}, reqBody = {}) { + const apiKey = this.getEtherscanApiKey(params, reqBody); + + if (apiKey && this.validateApiKey(apiKey)) { + this.setEtherscanApiKey(apiKey); + return apiKey; + } + + return null; + } +} + +module.exports = ApiKeyManager; diff --git a/backend/utils/constructorArgsGenerator.js b/backend/utils/constructorArgsGenerator.js new file mode 100644 index 0000000..37a5b6d --- /dev/null +++ b/backend/utils/constructorArgsGenerator.js @@ -0,0 +1,153 @@ +/** + * Централизованный генератор параметров конструктора для DLE контракта + * Обеспечивает одинаковые параметры для деплоя и верификации + */ + +/** + * Генерирует параметры конструктора для DLE контракта + * @param {Object} params - Параметры деплоя из базы данных + * @param {number} chainId - ID сети для деплоя (опционально) + * @returns {Object} Объект с параметрами конструктора + */ +function generateDLEConstructorArgs(params, chainId = null) { + // Валидация обязательных параметров + if (!params) { + throw new Error('Параметры деплоя не переданы'); + } + + // Базовые параметры DLE + const dleConfig = { + name: params.name || '', + symbol: params.symbol || '', + location: params.location || '', + coordinates: params.coordinates || '', + jurisdiction: params.jurisdiction || 0, + okvedCodes: params.okvedCodes || [], + kpp: params.kpp ? BigInt(params.kpp) : 0n, + quorumPercentage: params.quorumPercentage || 50, + initialPartners: params.initialPartners || [], + // Умножаем initialAmounts на 1e18 для конвертации в wei + initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount) * BigInt(1e18)), + supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id)) + }; + + // Определяем initializer + const initializer = params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000"; + + return { + dleConfig, + initializer + }; +} + +/** + * Генерирует параметры конструктора для верификации (с преобразованием в строки) + * @param {Object} params - Параметры деплоя из базы данных + * @param {number} chainId - ID сети для верификации (опционально) + * @returns {Array} Массив параметров конструктора для верификации + */ +function generateVerificationArgs(params, chainId = null) { + const { dleConfig, initializer } = generateDLEConstructorArgs(params, chainId); + + // Для верификации нужно преобразовать BigInt в строки + const verificationConfig = { + ...dleConfig, + initialAmounts: dleConfig.initialAmounts.map(amount => amount.toString()), + supportedChainIds: dleConfig.supportedChainIds.map(id => id.toString()) + }; + + return [ + verificationConfig, + initializer + ]; +} + +/** + * Генерирует параметры конструктора для деплоя (с BigInt) + * @param {Object} params - Параметры деплоя из базы данных + * @param {number} chainId - ID сети для деплоя (опционально) + * @returns {Object} Объект с параметрами конструктора для деплоя + */ +function generateDeploymentArgs(params, chainId = null) { + const { dleConfig, initializer } = generateDLEConstructorArgs(params, chainId); + + return { + dleConfig, + initializer + }; +} + +/** + * Валидирует параметры конструктора + * @param {Object} params - Параметры деплоя + * @returns {Object} Результат валидации + */ +function validateConstructorArgs(params) { + const errors = []; + const warnings = []; + + // Проверяем обязательные поля + if (!params.name) errors.push('name не указан'); + if (!params.symbol) errors.push('symbol не указан'); + if (!params.location) errors.push('location не указан'); + if (!params.coordinates) errors.push('coordinates не указаны'); + if (!params.jurisdiction) errors.push('jurisdiction не указан'); + if (!params.okvedCodes || !Array.isArray(params.okvedCodes)) errors.push('okvedCodes не указан или не является массивом'); + if (!params.initialPartners || !Array.isArray(params.initialPartners)) errors.push('initialPartners не указан или не является массивом'); + if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) errors.push('initialAmounts не указан или не является массивом'); + if (!params.supportedChainIds || !Array.isArray(params.supportedChainIds)) errors.push('supportedChainIds не указан или не является массивом'); + + // Проверяем соответствие массивов + if (params.initialPartners && params.initialAmounts && + params.initialPartners.length !== params.initialAmounts.length) { + errors.push('Количество initialPartners не соответствует количеству initialAmounts'); + } + + // Проверяем значения + if (params.quorumPercentage && (params.quorumPercentage < 1 || params.quorumPercentage > 100)) { + warnings.push('quorumPercentage должен быть от 1 до 100'); + } + + if (params.initialAmounts) { + const negativeAmounts = params.initialAmounts.filter(amount => amount < 0); + if (negativeAmounts.length > 0) { + errors.push('initialAmounts содержит отрицательные значения'); + } + } + + return { + isValid: errors.length === 0, + errors, + warnings + }; +} + +/** + * Логирует параметры конструктора для отладки + * @param {Object} params - Параметры деплоя + * @param {string} context - Контекст (deployment/verification) + */ +function logConstructorArgs(params, context = 'unknown') { + console.log(`📊 [${context.toUpperCase()}] Параметры конструктора:`); + console.log(` name: "${params.name}"`); + console.log(` symbol: "${params.symbol}"`); + console.log(` location: "${params.location}"`); + console.log(` coordinates: "${params.coordinates}"`); + console.log(` jurisdiction: ${params.jurisdiction}`); + console.log(` okvedCodes: [${params.okvedCodes.join(', ')}]`); + console.log(` kpp: ${params.kpp}`); + console.log(` quorumPercentage: ${params.quorumPercentage}`); + console.log(` initialPartners: [${params.initialPartners.join(', ')}]`); + console.log(` initialAmounts: [${params.initialAmounts.join(', ')}]`); + console.log(` supportedChainIds: [${params.supportedChainIds.join(', ')}]`); + console.log(` governanceChainId: 1 (Ethereum)`); + console.log(` initializer: ${params.initializer}`); +} + +module.exports = { + generateDLEConstructorArgs, + generateVerificationArgs, + generateDeploymentArgs, + validateConstructorArgs, + logConstructorArgs +}; diff --git a/backend/utils/deploymentUtils.js b/backend/utils/deploymentUtils.js new file mode 100644 index 0000000..4030ca0 --- /dev/null +++ b/backend/utils/deploymentUtils.js @@ -0,0 +1,226 @@ +/** + * Общие утилиты для деплоя контрактов + * Устраняет дублирование кода между скриптами деплоя + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const { ethers } = require('ethers'); +const logger = require('./logger'); +const RPCConnectionManager = require('./rpcConnectionManager'); +const { nonceManager } = require('./nonceManager'); + +/** + * Подбирает безопасные gas/fee для разных сетей (включая L2) + * @param {Object} provider - Провайдер ethers + * @param {Object} options - Опции для настройки + * @returns {Promise} - Объект с настройками газа + */ +async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { + try { + const fee = await provider.getFeeData(); + const overrides = {}; + const minPriority = await ethers.parseUnits(minPriorityGwei.toString(), 'gwei'); + const minFee = await ethers.parseUnits(minFeeGwei.toString(), 'gwei'); + + if (fee.maxFeePerGas) { + overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas; + overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n) + ? fee.maxPriorityFeePerGas + : minPriority; + } else if (fee.gasPrice) { + overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice; + } + + return overrides; + } catch (error) { + logger.error('Ошибка при получении fee overrides:', error); + throw error; + } +} + +/** + * Создает провайдер и кошелек для деплоя + * @param {string} rpcUrl - URL RPC + * @param {string} privateKey - Приватный ключ + * @returns {Object} - Объект с провайдером и кошельком + */ +function createProviderAndWallet(rpcUrl, privateKey) { + try { + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(privateKey, provider); + return { provider, wallet }; + } catch (error) { + logger.error('Ошибка при создании провайдера и кошелька:', error); + throw error; + } +} + +/** + * Выравнивает nonce до целевого значения + * @param {Object} wallet - Кошелек ethers + * @param {Object} provider - Провайдер ethers + * @param {number} targetNonce - Целевой nonce + * @param {Object} options - Опции для настройки + * @returns {Promise} - Текущий nonce после выравнивания + */ +async function alignNonce(wallet, provider, targetNonce, options = {}) { + try { + // Используем nonceManager для получения актуального nonce + const network = await provider.getNetwork(); + const chainId = Number(network.chainId); + const rpcUrl = provider._getConnection?.()?.url || 'unknown'; + + let current = await nonceManager.getNonceFast(wallet.address, rpcUrl, chainId); + + if (current > targetNonce) { + throw new Error(`Current nonce ${current} > target nonce ${targetNonce}`); + } + + if (current < targetNonce) { + logger.info(`Выравнивание nonce: ${current} -> ${targetNonce} (${targetNonce - current} транзакций)`); + + const { burnAddress = '0x000000000000000000000000000000000000dEaD' } = options; + + for (let i = current; i < targetNonce; i++) { + const overrides = await getFeeOverrides(provider); + const gasLimit = 21000n; + + try { + const txFill = await wallet.sendTransaction({ + to: burnAddress, + value: 0, + gasLimit, + ...overrides + }); + + logger.info(`Filler tx sent, hash=${txFill.hash}, nonce=${i}`); + + await txFill.wait(); + logger.info(`Filler tx confirmed, hash=${txFill.hash}, nonce=${i}`); + + // Обновляем nonce в кэше + nonceManager.reserveNonce(wallet.address, chainId, i); + current = i + 1; + } catch (error) { + logger.error(`Filler tx failed for nonce=${i}:`, error); + throw error; + } + } + + logger.info(`Nonce alignment completed, current nonce=${current}`); + } else { + logger.info(`Nonce already aligned at ${current}`); + } + + return current; + } catch (error) { + logger.error('Ошибка при выравнивании nonce:', error); + throw error; + } +} + +/** + * Получает информацию о сети + * @param {Object} provider - Провайдер ethers + * @returns {Promise} - Информация о сети + */ +async function getNetworkInfo(provider) { + try { + const network = await provider.getNetwork(); + return { + chainId: Number(network.chainId), + name: network.name + }; + } catch (error) { + logger.error('Ошибка при получении информации о сети:', error); + throw error; + } +} + +/** + * Проверяет баланс кошелька + * @param {Object} provider - Провайдер ethers + * @param {string} address - Адрес кошелька + * @returns {Promise} - Баланс в ETH + */ +async function getBalance(provider, address) { + try { + const balance = await provider.getBalance(address); + return ethers.formatEther(balance); + } catch (error) { + logger.error('Ошибка при получении баланса:', error); + throw error; + } +} + +/** + * Создает RPC соединение с retry логикой + * @param {string} rpcUrl - URL RPC + * @param {string} privateKey - Приватный ключ + * @param {Object} options - Опции соединения + * @returns {Promise} - {provider, wallet, network} + */ +async function createRPCConnection(rpcUrl, privateKey, options = {}) { + const rpcManager = new RPCConnectionManager(); + return await rpcManager.createConnection(rpcUrl, privateKey, options); +} + +/** + * Создает множественные RPC соединения с обработкой ошибок + * @param {Array} rpcUrls - Массив RPC URL + * @param {string} privateKey - Приватный ключ + * @param {Object} options - Опции соединения + * @returns {Promise} - Массив успешных соединений + */ +async function createMultipleRPCConnections(rpcUrls, privateKey, options = {}) { + const rpcManager = new RPCConnectionManager(); + return await rpcManager.createMultipleConnections(rpcUrls, privateKey, options); +} + +/** + * Выполняет транзакцию с retry логикой + * @param {Object} wallet - Кошелек + * @param {Object} txData - Данные транзакции + * @param {Object} options - Опции + * @returns {Promise} - Результат транзакции + */ +async function sendTransactionWithRetry(wallet, txData, options = {}) { + const rpcManager = new RPCConnectionManager(); + return await rpcManager.sendTransactionWithRetry(wallet, txData, options); +} + +/** + * Получает nonce с retry логикой + * @param {Object} provider - Провайдер + * @param {string} address - Адрес + * @param {Object} options - Опции + * @returns {Promise} - Nonce + */ +async function getNonceWithRetry(provider, address, options = {}) { + // Используем быстрый метод по умолчанию + if (options.fast !== false) { + try { + const network = await provider.getNetwork(); + const chainId = Number(network.chainId); + const rpcUrl = provider._getConnection?.()?.url || 'unknown'; + return await nonceManager.getNonceFast(address, rpcUrl, chainId); + } catch (error) { + console.warn(`[deploymentUtils] Быстрый nonce failed, используем retry: ${error.message}`); + } + } + + // Fallback на retry метод + return await nonceManager.getNonceWithRetry(provider, address, options); +} + +module.exports = { + getFeeOverrides, + createProviderAndWallet, + alignNonce, + getNetworkInfo, + getBalance, + createRPCConnection, + createMultipleRPCConnections, + sendTransactionWithRetry, + getNonceWithRetry +}; diff --git a/backend/utils/nonceManager.js b/backend/utils/nonceManager.js new file mode 100644 index 0000000..7f6df40 --- /dev/null +++ b/backend/utils/nonceManager.js @@ -0,0 +1,353 @@ +/** + * Менеджер nonce для управления транзакциями в разных сетях + * Решает проблему "nonce too low" при деплое в нескольких сетях + */ + +const { ethers } = require('ethers'); + +class NonceManager { + constructor() { + this.nonceCache = new Map(); // Кэш nonce для каждого адреса и сети + this.pendingTransactions = new Map(); // Отслеживание pending транзакций + } + + /** + * Получить актуальный nonce для адреса в сети с таймаутом и retry логикой + * @param {string} address - Адрес кошелька + * @param {string} rpcUrl - RPC URL сети + * @param {number} chainId - ID сети + * @param {Object} options - Опции (timeout, maxRetries) + * @returns {Promise} - Актуальный nonce + */ + async getNonce(address, rpcUrl, chainId, options = {}) { + const { timeout = 10000, maxRetries = 3 } = options; // Увеличиваем таймаут и попытки + + const cacheKey = `${address}-${chainId}`; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const provider = new ethers.JsonRpcProvider(rpcUrl); + + // Получаем nonce из сети с таймаутом + const networkNonce = await Promise.race([ + provider.getTransactionCount(address, 'pending'), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Nonce timeout')), timeout) + ) + ]); + + // ВАЖНО: Не используем кэш для критических операций деплоя + // Всегда получаем актуальный nonce из сети + this.nonceCache.set(cacheKey, networkNonce); + + console.log(`[NonceManager] ${address}:${chainId} nonce=${networkNonce} (попытка ${attempt})`); + + return networkNonce; + } catch (error) { + console.error(`[NonceManager] Ошибка ${address}:${chainId} (${attempt}):`, error.message); + + if (attempt === maxRetries) { + // В случае критической ошибки, сбрасываем кэш и пробуем еще раз + this.nonceCache.delete(cacheKey); + throw new Error(`Не удалось получить nonce после ${maxRetries} попыток: ${error.message}`); + } + + // Увеличиваем задержку между попытками + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + /** + * Зарезервировать nonce для транзакции + * @param {string} address - Адрес кошелька + * @param {number} chainId - ID сети + * @param {number} nonce - Nonce для резервирования + */ + reserveNonce(address, chainId, nonce) { + const cacheKey = `${address}-${chainId}`; + const currentNonce = this.nonceCache.get(cacheKey) || 0; + + if (nonce >= currentNonce) { + this.nonceCache.set(cacheKey, nonce + 1); + console.log(`[NonceManager] Зарезервирован nonce ${nonce} для ${address} в сети ${chainId}`); + } else { + console.warn(`[NonceManager] Попытка использовать nonce ${nonce} меньше текущего ${currentNonce} для ${address} в сети ${chainId}`); + } + } + + /** + * Отметить транзакцию как pending + * @param {string} address - Адрес кошелька + * @param {number} chainId - ID сети + * @param {number} nonce - Nonce транзакции + * @param {string} txHash - Хэш транзакции + */ + markTransactionPending(address, chainId, nonce, txHash) { + const cacheKey = `${address}-${chainId}`; + const pendingTxs = this.pendingTransactions.get(cacheKey) || []; + + pendingTxs.push({ + nonce, + txHash, + timestamp: Date.now() + }); + + this.pendingTransactions.set(cacheKey, pendingTxs); + console.log(`[NonceManager] Отмечена pending транзакция ${txHash} с nonce ${nonce} для ${address} в сети ${chainId}`); + } + + /** + * Отметить транзакцию как подтвержденную + * @param {string} address - Адрес кошелька + * @param {number} chainId - ID сети + * @param {string} txHash - Хэш транзакции + */ + markTransactionConfirmed(address, chainId, txHash) { + const cacheKey = `${address}-${chainId}`; + const pendingTxs = this.pendingTransactions.get(cacheKey) || []; + + const txIndex = pendingTxs.findIndex(tx => tx.txHash === txHash); + if (txIndex !== -1) { + const tx = pendingTxs[txIndex]; + pendingTxs.splice(txIndex, 1); + + console.log(`[NonceManager] Транзакция ${txHash} подтверждена для ${address} в сети ${chainId}`); + } + } + + /** + * Очистить старые pending транзакции + * @param {string} address - Адрес кошелька + * @param {number} chainId - ID сети + * @param {number} maxAge - Максимальный возраст в миллисекундах (по умолчанию 5 минут) + */ + clearOldPendingTransactions(address, chainId, maxAge = 5 * 60 * 1000) { + const cacheKey = `${address}-${chainId}`; + const pendingTxs = this.pendingTransactions.get(cacheKey) || []; + const now = Date.now(); + + const validTxs = pendingTxs.filter(tx => (now - tx.timestamp) < maxAge); + + if (validTxs.length !== pendingTxs.length) { + this.pendingTransactions.set(cacheKey, validTxs); + console.log(`[NonceManager] Очищено ${pendingTxs.length - validTxs.length} старых pending транзакций для ${address} в сети ${chainId}`); + } + } + + /** + * Получить информацию о pending транзакциях + * @param {string} address - Адрес кошелька + * @param {number} chainId - ID сети + * @returns {Array} - Массив pending транзакций + */ + getPendingTransactions(address, chainId) { + const cacheKey = `${address}-${chainId}`; + return this.pendingTransactions.get(cacheKey) || []; + } + + /** + * Сбросить кэш nonce для адреса и сети + * @param {string} address - Адрес кошелька + * @param {number} chainId - ID сети + */ + resetNonce(address, chainId) { + const cacheKey = `${address}-${chainId}`; + this.nonceCache.delete(cacheKey); + this.pendingTransactions.delete(cacheKey); + console.log(`[NonceManager] Сброшен кэш nonce для ${address} в сети ${chainId}`); + } + + /** + * Получить статистику по nonce + * @returns {Object} - Статистика + */ + getStats() { + return { + nonceCache: Object.fromEntries(this.nonceCache), + pendingTransactions: Object.fromEntries(this.pendingTransactions) + }; + } + + /** + * Быстрое получение nonce без retry (для критичных по времени операций) + * @param {string} address - Адрес кошелька + * @param {string} rpcUrl - RPC URL сети + * @param {number} chainId - ID сети + * @returns {Promise} - Nonce + */ + async getNonceFast(address, rpcUrl, chainId) { + const cacheKey = `${address}-${chainId}`; + const cachedNonce = this.nonceCache.get(cacheKey); + + if (cachedNonce !== undefined) { + console.log(`[NonceManager] Быстрый nonce из кэша: ${cachedNonce} для ${address}:${chainId}`); + return cachedNonce; + } + + // Получаем RPC URLs из базы данных с fallback + const rpcUrls = await this.getRpcUrlsFromDatabase(chainId, rpcUrl); + + for (const currentRpc of rpcUrls) { + try { + console.log(`[NonceManager] Пробуем RPC: ${currentRpc}`); + const provider = new ethers.JsonRpcProvider(currentRpc); + + const networkNonce = await Promise.race([ + provider.getTransactionCount(address, 'pending'), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Fast nonce timeout')), 3000) + ) + ]); + + this.nonceCache.set(cacheKey, networkNonce); + console.log(`[NonceManager] ✅ Nonce получен: ${networkNonce} для ${address}:${chainId} с RPC: ${currentRpc}`); + return networkNonce; + } catch (error) { + console.warn(`[NonceManager] RPC failed: ${currentRpc} - ${error.message}`); + continue; + } + } + + // Если все RPC недоступны, возвращаем 0 + console.warn(`[NonceManager] Все RPC недоступны для ${address}:${chainId}, возвращаем 0`); + this.nonceCache.set(cacheKey, 0); + return 0; + } + + /** + * Получить RPC URLs из базы данных с fallback + * @param {number} chainId - ID сети + * @param {string} primaryRpcUrl - Основной RPC URL (опциональный) + * @returns {Promise} - Массив RPC URL + */ + async getRpcUrlsFromDatabase(chainId, primaryRpcUrl = null) { + const rpcUrls = []; + + // Добавляем основной RPC URL если указан + if (primaryRpcUrl) { + rpcUrls.push(primaryRpcUrl); + } + + try { + // Получаем RPC из deploy_params (как в deploy-multichain.js) + const DeployParamsService = require('../services/deployParamsService'); + const deployParamsService = new DeployParamsService(); + + // Получаем последние параметры деплоя + const latestParams = await deployParamsService.getLatestDeployParams(1); + if (latestParams.length > 0) { + const params = latestParams[0]; + const supportedChainIds = params.supported_chain_ids || []; + const rpcUrlsFromParams = params.rpc_urls || []; + + // Находим RPC для нужного chainId + const chainIndex = supportedChainIds.indexOf(chainId); + if (chainIndex !== -1 && rpcUrlsFromParams[chainIndex]) { + const deployRpcUrl = rpcUrlsFromParams[chainIndex]; + if (!rpcUrls.includes(deployRpcUrl)) { + rpcUrls.push(deployRpcUrl); + console.log(`[NonceManager] ✅ RPC из deploy_params для chainId ${chainId}: ${deployRpcUrl}`); + } + } + } + + await deployParamsService.close(); + } catch (error) { + console.warn(`[NonceManager] deploy_params недоступны для chainId ${chainId}, используем fallback: ${error.message}`); + } + + // Всегда добавляем fallback RPC для надежности + const fallbackRPCs = this.getFallbackRPCs(chainId); + for (const fallbackRpc of fallbackRPCs) { + if (!rpcUrls.includes(fallbackRpc)) { + rpcUrls.push(fallbackRpc); + } + } + + console.log(`[NonceManager] RPC URLs для chainId ${chainId}:`, rpcUrls); + return rpcUrls; + } + + /** + * Получить список fallback RPC для сети + * @param {number} chainId - ID сети + * @returns {Array} - Массив RPC URL + */ + getFallbackRPCs(chainId) { + const fallbackRPCs = { + 1: [ // Mainnet + 'https://eth.llamarpc.com', + 'https://rpc.ankr.com/eth', + 'https://ethereum.publicnode.com' + ], + 11155111: [ // Sepolia + 'https://rpc.sepolia.org', + 'https://sepolia.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161' + ], + 17000: [ // Holesky + 'https://ethereum-holesky.publicnode.com', + 'https://holesky.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161' + ], + 421614: [ // Arbitrum Sepolia + 'https://sepolia-rollup.arbitrum.io/rpc' + ], + 84532: [ // Base Sepolia + 'https://sepolia.base.org' + ] + }; + + return fallbackRPCs[chainId] || []; + } + + /** + * Интеграция с существующими системами - замена для rpcConnectionManager + * @param {Object} provider - Провайдер ethers + * @param {string} address - Адрес кошелька + * @param {Object} options - Опции + * @returns {Promise} - Nonce + */ + async getNonceWithRetry(provider, address, options = {}) { + // Извлекаем chainId из провайдера + const network = await provider.getNetwork(); + const chainId = Number(network.chainId); + + // Получаем RPC URL из провайдера (если возможно) + const rpcUrl = provider._getConnection?.()?.url || 'unknown'; + + return await this.getNonce(address, rpcUrl, chainId, options); + } + + /** + * Принудительно обновляет nonce из сети (для обработки race conditions) + * @param {string} address - Адрес кошелька + * @param {string} rpcUrl - RPC URL сети + * @param {number} chainId - ID сети + * @returns {Promise} - Актуальный nonce + */ + async forceRefreshNonce(address, rpcUrl, chainId) { + const cacheKey = `${address}-${chainId}`; + + try { + const provider = new ethers.JsonRpcProvider(rpcUrl); + const networkNonce = await provider.getTransactionCount(address, 'pending'); + + // Принудительно обновляем кэш + this.nonceCache.set(cacheKey, networkNonce); + + console.log(`[NonceManager] Force refreshed nonce for ${address}:${chainId} = ${networkNonce}`); + return networkNonce; + } catch (error) { + console.error(`[NonceManager] Force refresh failed for ${address}:${chainId}:`, error.message); + throw error; + } + } +} + +// Создаем глобальный экземпляр +const nonceManager = new NonceManager(); + +module.exports = { + NonceManager, + nonceManager +}; diff --git a/backend/utils/operationDecoder.js b/backend/utils/operationDecoder.js new file mode 100644 index 0000000..4708834 --- /dev/null +++ b/backend/utils/operationDecoder.js @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + * All rights reserved. + * + * This software is proprietary and confidential. + * Unauthorized copying, modification, or distribution is prohibited. + * + * For licensing inquiries: info@hb3-accelerator.com + * Website: https://hb3-accelerator.com + * GitHub: https://github.com/HB3-ACCELERATOR + */ + +const { ethers } = require('ethers'); + +/** + * Декодирует операцию из формата abi.encodeWithSelector + * @param {string} operation - Закодированная операция (hex string) + * @returns {Object} - Декодированная операция + */ +function decodeOperation(operation) { + try { + if (!operation || operation.length < 4) { + return { + type: 'unknown', + selector: null, + data: null, + decoded: null, + error: 'Invalid operation format' + }; + } + + // Извлекаем селектор (первые 4 байта) + const selector = operation.slice(0, 10); // 0x + 4 байта + const data = operation.slice(10); // Остальные данные + + // Определяем тип операции по селектору + const operationType = getOperationType(selector); + + if (operationType === 'unknown') { + return { + type: 'unknown', + selector: selector, + data: data, + decoded: null, + error: 'Unknown operation selector' + }; + } + + // Декодируем данные в зависимости от типа операции + let decoded = null; + try { + decoded = decodeOperationData(operationType, data); + } catch (decodeError) { + return { + type: operationType, + selector: selector, + data: data, + decoded: null, + error: `Failed to decode ${operationType}: ${decodeError.message}` + }; + } + + return { + type: operationType, + selector: selector, + data: data, + decoded: decoded, + error: null + }; + + } catch (error) { + return { + type: 'error', + selector: null, + data: null, + decoded: null, + error: error.message + }; + } +} + +/** + * Определяет тип операции по селектору + * @param {string} selector - Селектор функции (0x + 4 байта) + * @returns {string} - Тип операции + */ +function getOperationType(selector) { + const selectors = { + '0x12345678': '_addModule', // Пример селектора + '0x87654321': '_removeModule', // Пример селектора + '0xabcdef12': '_addSupportedChain', // Пример селектора + '0x21fedcba': '_removeSupportedChain', // Пример селектора + '0x1234abcd': '_transferTokens', // Пример селектора + '0xabcd1234': '_updateVotingDurations', // Пример селектора + '0x5678efgh': '_setLogoURI', // Пример селектора + '0xefgh5678': '_updateQuorumPercentage', // Пример селектора + '0x9abc1234': '_updateDLEInfo', // Пример селектора + '0x12349abc': 'offchainAction' // Пример селектора + }; + + // Вычисляем реальные селекторы + const realSelectors = { + [ethers.id('_addModule(bytes32,address)').slice(0, 10)]: '_addModule', + [ethers.id('_removeModule(bytes32)').slice(0, 10)]: '_removeModule', + [ethers.id('_addSupportedChain(uint256)').slice(0, 10)]: '_addSupportedChain', + [ethers.id('_removeSupportedChain(uint256)').slice(0, 10)]: '_removeSupportedChain', + [ethers.id('_transferTokens(address,uint256)').slice(0, 10)]: '_transferTokens', + [ethers.id('_updateVotingDurations(uint256,uint256)').slice(0, 10)]: '_updateVotingDurations', + [ethers.id('_setLogoURI(string)').slice(0, 10)]: '_setLogoURI', + [ethers.id('_updateQuorumPercentage(uint256)').slice(0, 10)]: '_updateQuorumPercentage', + [ethers.id('_updateDLEInfo(string,string,string,string,uint256,string[],uint256)').slice(0, 10)]: '_updateDLEInfo', + [ethers.id('offchainAction(bytes32,string,bytes32)').slice(0, 10)]: 'offchainAction' + }; + + return realSelectors[selector] || 'unknown'; +} + +/** + * Декодирует данные операции в зависимости от типа + * @param {string} operationType - Тип операции + * @param {string} data - Закодированные данные + * @returns {Object} - Декодированные данные + */ +function decodeOperationData(operationType, data) { + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + + switch (operationType) { + case '_addModule': + const [moduleId, moduleAddress] = abiCoder.decode(['bytes32', 'address'], '0x' + data); + return { + moduleId: moduleId, + moduleAddress: moduleAddress + }; + + case '_removeModule': + const [moduleIdToRemove] = abiCoder.decode(['bytes32'], '0x' + data); + return { + moduleId: moduleIdToRemove + }; + + case '_addSupportedChain': + const [chainIdToAdd] = abiCoder.decode(['uint256'], '0x' + data); + return { + chainId: Number(chainIdToAdd) + }; + + case '_removeSupportedChain': + const [chainIdToRemove] = abiCoder.decode(['uint256'], '0x' + data); + return { + chainId: Number(chainIdToRemove) + }; + + case '_transferTokens': + const [recipient, amount] = abiCoder.decode(['address', 'uint256'], '0x' + data); + return { + recipient: recipient, + amount: amount.toString(), + amountFormatted: ethers.formatEther(amount) + }; + + case '_updateVotingDurations': + const [minDuration, maxDuration] = abiCoder.decode(['uint256', 'uint256'], '0x' + data); + return { + minDuration: Number(minDuration), + maxDuration: Number(maxDuration) + }; + + case '_setLogoURI': + const [logoURI] = abiCoder.decode(['string'], '0x' + data); + return { + logoURI: logoURI + }; + + case '_updateQuorumPercentage': + const [quorumPercentage] = abiCoder.decode(['uint256'], '0x' + data); + return { + quorumPercentage: Number(quorumPercentage) + }; + + case '_updateDLEInfo': + const [name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp] = abiCoder.decode( + ['string', 'string', 'string', 'string', 'uint256', 'string[]', 'uint256'], + '0x' + data + ); + return { + name: name, + symbol: symbol, + location: location, + coordinates: coordinates, + jurisdiction: Number(jurisdiction), + okvedCodes: okvedCodes, + kpp: Number(kpp) + }; + + case 'offchainAction': + const [actionId, kind, payloadHash] = abiCoder.decode(['bytes32', 'string', 'bytes32'], '0x' + data); + return { + actionId: actionId, + kind: kind, + payloadHash: payloadHash + }; + + default: + throw new Error(`Unknown operation type: ${operationType}`); + } +} + +/** + * Форматирует декодированную операцию для отображения + * @param {Object} decodedOperation - Декодированная операция + * @returns {string} - Отформатированное описание + */ +function formatOperation(decodedOperation) { + if (decodedOperation.error) { + return `Ошибка: ${decodedOperation.error}`; + } + + const { type, decoded } = decodedOperation; + + switch (type) { + case '_addModule': + return `Добавить модуль: ${decoded.moduleId} (${decoded.moduleAddress})`; + + case '_removeModule': + return `Удалить модуль: ${decoded.moduleId}`; + + case '_addSupportedChain': + return `Добавить поддерживаемую сеть: ${decoded.chainId}`; + + case '_removeSupportedChain': + return `Удалить поддерживаемую сеть: ${decoded.chainId}`; + + case '_transferTokens': + return `Перевести токены: ${decoded.amountFormatted} DLE на адрес ${decoded.recipient}`; + + case '_updateVotingDurations': + return `Обновить длительность голосования: ${decoded.minDuration}-${decoded.maxDuration} секунд`; + + case '_setLogoURI': + return `Обновить логотип: ${decoded.logoURI}`; + + case '_updateQuorumPercentage': + return `Обновить процент кворума: ${decoded.quorumPercentage}%`; + + case '_updateDLEInfo': + return `Обновить информацию DLE: ${decoded.name} (${decoded.symbol})`; + + case 'offchainAction': + return `Оффчейн действие: ${decoded.kind} (${decoded.actionId})`; + + default: + return `Неизвестная операция: ${type}`; + } +} + +/** + * Получает название сети по ID + * @param {number} chainId - ID сети + * @returns {string} - Название сети + */ +function getChainName(chainId) { + const chainNames = { + 1: 'Ethereum Mainnet', + 11155111: 'Sepolia', + 17000: 'Holesky', + 421614: 'Arbitrum Sepolia', + 84532: 'Base Sepolia', + 137: 'Polygon', + 80001: 'Polygon Mumbai', + 56: 'BSC', + 97: 'BSC Testnet' + }; + + return chainNames[chainId] || `Chain ${chainId}`; +} + +module.exports = { + decodeOperation, + formatOperation, + getChainName +}; diff --git a/backend/utils/rpcConnectionManager.js b/backend/utils/rpcConnectionManager.js new file mode 100644 index 0000000..14fb050 --- /dev/null +++ b/backend/utils/rpcConnectionManager.js @@ -0,0 +1,250 @@ +/** + * Менеджер RPC соединений с retry логикой и обработкой ошибок + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + */ + +const { ethers } = require('ethers'); +const logger = require('./logger'); + +class RPCConnectionManager { + constructor() { + this.connections = new Map(); // Кэш соединений + this.retryConfig = { + maxRetries: 3, + baseDelay: 1000, // 1 секунда + maxDelay: 10000, // 10 секунд + timeout: 30000 // 30 секунд + }; + } + + /** + * Создает RPC соединение с retry логикой + * @param {string} rpcUrl - URL RPC + * @param {string} privateKey - Приватный ключ + * @param {Object} options - Опции соединения + * @returns {Promise} - {provider, wallet, network} + */ + async createConnection(rpcUrl, privateKey, options = {}) { + const config = { ...this.retryConfig, ...options }; + const connectionKey = `${rpcUrl}_${privateKey}`; + + // Проверяем кэш + if (this.connections.has(connectionKey)) { + const cached = this.connections.get(connectionKey); + if (Date.now() - cached.timestamp < 60000) { // 1 минута кэш + logger.info(`[RPC_MANAGER] Используем кэшированное соединение: ${rpcUrl}`); + return cached.connection; + } + } + + logger.info(`[RPC_MANAGER] Создаем новое RPC соединение: ${rpcUrl}`); + + for (let attempt = 1; attempt <= config.maxRetries; attempt++) { + try { + const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { + polling: false, + staticNetwork: false + }); + + // Проверяем соединение с timeout + const network = await Promise.race([ + provider.getNetwork(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('RPC timeout')), config.timeout) + ) + ]); + + const wallet = new ethers.Wallet(privateKey, provider); + + const connection = { provider, wallet, network }; + + // Кэшируем соединение + this.connections.set(connectionKey, { + connection, + timestamp: Date.now() + }); + + logger.info(`[RPC_MANAGER] ✅ RPC соединение установлено: ${rpcUrl} (chainId: ${network.chainId})`); + return connection; + + } catch (error) { + logger.error(`[RPC_MANAGER] ❌ Попытка ${attempt}/${config.maxRetries} failed: ${error.message}`); + + if (attempt === config.maxRetries) { + throw new Error(`RPC соединение не удалось установить после ${config.maxRetries} попыток: ${error.message}`); + } + + // Экспоненциальная задержка + const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay); + logger.info(`[RPC_MANAGER] Ожидание ${delay}ms перед повторной попыткой...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + /** + * Создает множественные RPC соединения с обработкой ошибок + * @param {Array} rpcUrls - Массив RPC URL + * @param {string} privateKey - Приватный ключ + * @param {Object} options - Опции соединения + * @returns {Promise} - Массив успешных соединений + */ + async createMultipleConnections(rpcUrls, privateKey, options = {}) { + logger.info(`[RPC_MANAGER] Создаем ${rpcUrls.length} RPC соединений...`); + + const connectionPromises = rpcUrls.map(async (rpcUrl, index) => { + try { + const connection = await this.createConnection(rpcUrl, privateKey, options); + return { index, rpcUrl, ...connection, success: true }; + } catch (error) { + logger.error(`[RPC_MANAGER] ❌ Соединение ${index + 1} failed: ${rpcUrl} - ${error.message}`); + return { index, rpcUrl, error: error.message, success: false }; + } + }); + + const results = await Promise.all(connectionPromises); + const successful = results.filter(r => r.success); + const failed = results.filter(r => !r.success); + + logger.info(`[RPC_MANAGER] ✅ Успешных соединений: ${successful.length}/${rpcUrls.length}`); + if (failed.length > 0) { + logger.warn(`[RPC_MANAGER] ⚠️ Неудачных соединений: ${failed.length}`); + failed.forEach(f => logger.warn(`[RPC_MANAGER] - ${f.rpcUrl}: ${f.error}`)); + } + + if (successful.length === 0) { + throw new Error('Не удалось установить ни одного RPC соединения'); + } + + return successful; + } + + /** + * Выполняет транзакцию с retry логикой + * @param {Object} wallet - Кошелек + * @param {Object} txData - Данные транзакции + * @param {Object} options - Опции + * @returns {Promise} - Результат транзакции + */ + async sendTransactionWithRetry(wallet, txData, options = {}) { + const config = { ...this.retryConfig, ...options }; + + for (let attempt = 1; attempt <= config.maxRetries; attempt++) { + try { + logger.info(`[RPC_MANAGER] Отправка транзакции (попытка ${attempt}/${config.maxRetries})`); + + const tx = await wallet.sendTransaction({ + ...txData, + timeout: config.timeout + }); + + logger.info(`[RPC_MANAGER] ✅ Транзакция отправлена: ${tx.hash}`); + + // Ждем подтверждения с timeout + const receipt = await Promise.race([ + tx.wait(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Transaction timeout')), config.timeout) + ) + ]); + + logger.info(`[RPC_MANAGER] ✅ Транзакция подтверждена: ${tx.hash}`); + return { tx, receipt, success: true }; + + } catch (error) { + logger.error(`[RPC_MANAGER] ❌ Транзакция failed (попытка ${attempt}): ${error.message}`); + + if (attempt === config.maxRetries) { + throw new Error(`Транзакция не удалась после ${config.maxRetries} попыток: ${error.message}`); + } + + // Проверяем, стоит ли повторять + if (this.shouldRetry(error)) { + const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay); + logger.info(`[RPC_MANAGER] Ожидание ${delay}ms перед повторной попыткой...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + } + + /** + * Определяет, стоит ли повторять операцию + * @param {Error} error - Ошибка + * @returns {boolean} - Стоит ли повторять + */ + shouldRetry(error) { + const retryableErrors = [ + 'NETWORK_ERROR', + 'TIMEOUT', + 'ECONNRESET', + 'ENOTFOUND', + 'ETIMEDOUT', + 'RPC timeout', + 'Transaction timeout' + ]; + + const errorMessage = error.message.toLowerCase(); + return retryableErrors.some(retryableError => + errorMessage.includes(retryableError.toLowerCase()) + ); + } + + /** + * Получает nonce с retry логикой + * @param {Object} provider - Провайдер + * @param {string} address - Адрес + * @param {Object} options - Опции + * @returns {Promise} - Nonce + */ + async getNonceWithRetry(provider, address, options = {}) { + const config = { ...this.retryConfig, ...options }; + + for (let attempt = 1; attempt <= config.maxRetries; attempt++) { + try { + const nonce = await Promise.race([ + provider.getTransactionCount(address, 'pending'), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Nonce timeout')), config.timeout) + ) + ]); + + logger.info(`[RPC_MANAGER] ✅ Nonce получен: ${nonce} (попытка ${attempt})`); + return nonce; + + } catch (error) { + logger.error(`[RPC_MANAGER] ❌ Nonce failed (попытка ${attempt}): ${error.message}`); + + if (attempt === config.maxRetries) { + throw new Error(`Не удалось получить nonce после ${config.maxRetries} попыток: ${error.message}`); + } + + const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + /** + * Очищает кэш соединений + */ + clearCache() { + this.connections.clear(); + logger.info('[RPC_MANAGER] Кэш соединений очищен'); + } + + /** + * Получает статистику соединений + * @returns {Object} - Статистика + */ + getStats() { + return { + cachedConnections: this.connections.size, + retryConfig: this.retryConfig + }; + } +} + +module.exports = RPCConnectionManager; diff --git a/backend/wsHub.js b/backend/wsHub.js index 42e1981..b8bb61f 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -96,6 +96,9 @@ function initWSS(server) { wsClients.delete(userId); } } + + // Удаляем клиента из deploymentWebSocketService + deploymentWebSocketService.removeClient(ws); }); ws.on('error', (error) => { @@ -494,7 +497,7 @@ function broadcastDeploymentUpdate(data) { } }); - console.log(`📡 [WebSocket] Отправлено deployment update: ${data.type || 'unknown'}`); + console.log(`📡 [WebSocket] Отправлено deployment update: deployment_update`); } // Функция для уведомления об обновлениях модулей diff --git a/backend/yarn.lock b/backend/yarn.lock index 544ac19..58dfa85 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -8,9 +8,9 @@ integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== "@adraffy/ens-normalize@^1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" - integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + version "1.11.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz#6c2d657d4b2dfb37f8ea811dcb3e60843d4ac24a" + integrity sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ== "@anthropic-ai/sdk@^0.51.0": version "0.51.0" @@ -45,9 +45,9 @@ tough-cookie "^4.1.3" "@cypress/request@^3.0.1": - version "3.0.8" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.8.tgz#992f1f42ba03ebb14fa5d97290abe9d015ed0815" - integrity sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ== + version "3.0.9" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.9.tgz#8ed6e08fea0c62998b5552301023af7268f11625" + integrity sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -55,7 +55,7 @@ combined-stream "~1.0.6" extend "~3.0.2" forever-agent "~0.6.1" - form-data "~4.0.0" + form-data "~4.0.4" http-signature "~1.4.0" is-typedarray "~1.0.0" isstream "~0.1.2" @@ -77,10 +77,10 @@ enabled "2.0.x" kuler "^2.0.0" -"@eslint-community/eslint-utils@^4.2.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" - integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== +"@eslint-community/eslint-utils@^4.8.0": + version "4.9.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" @@ -98,22 +98,15 @@ debug "^4.3.1" minimatch "^3.1.2" -"@eslint/config-helpers@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.0.tgz#3e09a90dfb87e0005c7694791e58e97077271286" - integrity sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw== +"@eslint/config-helpers@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.1.tgz#d316e47905bd0a1a931fa50e669b9af4104d1617" + integrity sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA== -"@eslint/core@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.14.0.tgz#326289380968eaf7e96f364e1e4cf8f3adf2d003" - integrity sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg== - dependencies: - "@types/json-schema" "^7.0.15" - -"@eslint/core@^0.15.1": - version "0.15.1" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.1.tgz#d530d44209cbfe2f82ef86d6ba08760196dd3b60" - integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA== +"@eslint/core@^0.15.2": + version "0.15.2" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.2.tgz#59386327d7862cc3603ebc7c78159d2dcc4a868f" + integrity sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg== dependencies: "@types/json-schema" "^7.0.15" @@ -132,22 +125,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.30.1": - version "9.30.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.30.1.tgz#ebe9dd52a38345784c486300175a28c6013c088d" - integrity sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg== +"@eslint/js@9.36.0": + version "9.36.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.36.0.tgz#b1a3893dd6ce2defed5fd49de805ba40368e8fef" + integrity sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw== "@eslint/object-schema@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.3.1": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd" - integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag== +"@eslint/plugin-kit@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz#fd8764f0ee79c8ddab4da65460c641cefee017c5" + integrity sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w== dependencies: - "@eslint/core" "^0.15.1" + "@eslint/core" "^0.15.2" levn "^0.4.1" "@ethereumjs/rlp@^4.0.1": @@ -380,14 +373,12 @@ integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== "@google/genai@^1.0.1": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.8.0.tgz#b99d776bfc83160431240b79b8eb57526cb8fbdc" - integrity sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg== + version "1.21.0" + resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.21.0.tgz#3385514882bcf6cf5cb1d3ec218422a7b2fa56f1" + integrity sha512-k47DECR8BF9z7IJxQd3reKuH2eUnOH5NlJWSe+CKM6nbXx+wH3hmtWQxUQR9M8gzWW1EvFuRVgjQssEIreNZsw== dependencies: google-auth-library "^9.14.2" ws "^8.18.0" - zod "^3.22.4" - zod-to-json-schema "^3.22.4" "@graphql-typed-document-node/core@^3.2.0": version "3.2.0" @@ -395,21 +386,21 @@ integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== "@grpc/grpc-js@^1.13.1": - version "1.13.4" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.13.4.tgz#922fbc496e229c5fa66802d2369bf181c1df1c5a" - integrity sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg== + version "1.14.0" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.0.tgz#a3c47e7816ca2b4d5490cba9e06a3cf324e675ad" + integrity sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg== dependencies: - "@grpc/proto-loader" "^0.7.13" + "@grpc/proto-loader" "^0.8.0" "@js-sdsl/ordered-map" "^4.4.2" -"@grpc/proto-loader@^0.7.13": - version "0.7.15" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" - integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== +"@grpc/proto-loader@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" + integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== dependencies: lodash.camelcase "^4.3.0" long "^5.0.0" - protobufjs "^7.2.5" + protobufjs "^7.5.3" yargs "^17.7.2" "@humanfs/core@^0.19.1": @@ -418,24 +409,19 @@ integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== "@humanfs/node@^0.16.6": - version "0.16.6" - resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e" - integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw== + version "0.16.7" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== dependencies: "@humanfs/core" "^0.19.1" - "@humanwhocodes/retry" "^0.3.0" + "@humanwhocodes/retry" "^0.4.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/retry@^0.3.0": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a" - integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA== - -"@humanwhocodes/retry@^0.4.2": +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": version "0.4.3" resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== @@ -470,9 +456,9 @@ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/sourcemap-codec@^1.4.10": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz#7358043433b2e5da569aa02cbc4c121da3af27d7" - integrity sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw== + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" @@ -488,18 +474,18 @@ integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== "@langchain/community@^0.3.34": - version "0.3.48" - resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.48.tgz#3487ada66da38ed47e2e1fa08d7299264a9be8bf" - integrity sha512-0KceBKSx34lL5cnbKybWIMQAFTgkZJMOzcZ1YdcagIwgoDa5a4MsJdtTABxaY0gu+87Uo3KqMj+GXx2wQqnZWA== + version "0.3.56" + resolved "https://registry.yarnpkg.com/@langchain/community/-/community-0.3.56.tgz#ed01e18e89c5515ed241c381c47eb689b0a63395" + integrity sha512-lDjUnRfHAX7aMXyEB2EWbe5qOmdQdz8n+0CNQ4ExpLy3NOFQhEVkWclhsucaX04zh0r/VH5Pkk9djpnhPBDH7g== dependencies: - "@langchain/openai" ">=0.2.0 <0.6.0" + "@langchain/openai" ">=0.2.0 <0.7.0" "@langchain/weaviate" "^0.2.0" binary-extensions "^2.2.0" expr-eval "^2.0.2" flat "^5.0.2" js-yaml "^4.1.0" langchain ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0" - langsmith "^0.3.33" + langsmith "^0.3.67" uuid "^10.0.0" zod "^3.25.32" @@ -521,20 +507,20 @@ zod-to-json-schema "^3.22.3" "@langchain/ollama@^0.2.0": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.3.tgz#4868e66db4fc480f08c42fc652274abbab0416f0" - integrity sha512-1Obe45jgQspqLMBVlayQbGdywFmri8DgmGRdzNu0li56cG5RReYlRCFVDZBRMMvF9JhsP5eXRyfyivtKfITHWQ== + version "0.2.4" + resolved "https://registry.yarnpkg.com/@langchain/ollama/-/ollama-0.2.4.tgz#91c2108015e018f1dcae1207c8bc44da0cf047fa" + integrity sha512-XThDrZurNPcUO6sasN13rkes1aGgu5gWAtDkkyIGT3ZeMOvrYgPKGft+bbhvsigTIH9C01TfPzrSp8LAmvHIjA== dependencies: - ollama "^0.5.12" + ollama "^0.5.17" uuid "^10.0.0" -"@langchain/openai@>=0.1.0 <0.6.0", "@langchain/openai@>=0.2.0 <0.6.0": - version "0.5.18" - resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.5.18.tgz#59ebbf48044d711ce9503d3b9854a3533cb54683" - integrity sha512-CX1kOTbT5xVFNdtLjnM0GIYNf+P7oMSu+dGCFxxWRa3dZwWiuyuBXCm+dToUGxDLnsHuV1bKBtIzrY1mLq/A1Q== +"@langchain/openai@>=0.1.0 <0.7.0", "@langchain/openai@>=0.2.0 <0.7.0": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@langchain/openai/-/openai-0.6.13.tgz#87662cc27ee22ef075e2c7bbc9050c8a25fb0007" + integrity sha512-+QCVag3J2MeFxLMPjjYYpDCBKbmrK7D/xQGq+iWBGpNSg/08vnx7pEkkhiL2NTFIHiYu7w/7EG3UHQ8gOK/cag== dependencies: js-tiktoken "^1.0.12" - openai "^5.3.0" + openai "5.12.2" zod "^3.25.32" "@langchain/textsplitters@>=0.0.0 <0.2.0": @@ -545,9 +531,9 @@ js-tiktoken "^1.0.12" "@langchain/weaviate@^0.2.0": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@langchain/weaviate/-/weaviate-0.2.1.tgz#51ad20cf6d40e63d6149e5d01f91597cdff66744" - integrity sha512-rlfAKF+GB0A5MUrol34oDrBkl4q6AefARk9KDW+LfzhV/74pZZLZyIPYPxvE4XwI3gvpwp024DNsDxK/4UW0/g== + version "0.2.3" + resolved "https://registry.yarnpkg.com/@langchain/weaviate/-/weaviate-0.2.3.tgz#7b2557ea9a369bb7ce05dfc553f56b6ff062d90b" + integrity sha512-WqNGn1eSrI+ZigJd7kZjCj3fvHBYicKr054qts2nNJ+IyO5dWmY3oFTaVHFq1OLFVZJJxrFeDnxSEOC3JnfP0w== dependencies: uuid "^10.0.0" weaviate-client "^3.5.2" @@ -571,10 +557,10 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/curves@1.9.2", "@noble/curves@^1.9.1", "@noble/curves@~1.9.0": - version "1.9.2" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" - integrity sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g== +"@noble/curves@1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.1.tgz#9654a0bc6c13420ae252ddcf975eaf0f58f0a35c" + integrity sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA== dependencies: "@noble/hashes" "1.8.0" @@ -585,6 +571,13 @@ dependencies: "@noble/hashes" "1.7.2" +"@noble/curves@~1.9.0": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.7.tgz#79d04b4758a43e4bca2cbdc62e7771352fa6b951" + integrity sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== + dependencies: + "@noble/hashes" "1.8.0" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" @@ -690,9 +683,9 @@ "@nomicfoundation/edr-win32-x64-msvc" "0.11.3" "@nomicfoundation/hardhat-chai-matchers@^2.0.0": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.0.9.tgz#dfc0ec3aa0d085e55c20322696211fbb0620124f" - integrity sha512-AbCoBuTKMlwlf1lesSmi/4VvJHNG9EP13EmkCJ+MJS1SBdtVtU4YrBbdYmnYPEvRFcAIMFB/cwcQGmuBYeCoVg== + version "2.1.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-chai-matchers/-/hardhat-chai-matchers-2.1.0.tgz#c651dfcacd7b374f9a9876a6d5bcf459a86c4c82" + integrity sha512-GPhBNafh1fCnVD9Y7BYvoLnblnvfcq3j8YDbO1gGe/1nOFWzGmV7gFu5DkwFXF+IpYsS+t96o9qc/mPu3V3Vfw== dependencies: "@types/chai-as-promised" "^7.1.3" chai-as-promised "^7.1.1" @@ -700,25 +693,25 @@ ordinal "^1.0.3" "@nomicfoundation/hardhat-ethers@^3.0.0": - version "3.0.9" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.0.9.tgz#a1fa5b123db39e4ee4ae86bee0e458e2b733ce02" - integrity sha512-xBJdRUiCwKpr0OYrOzPwAyNGtsVzoBx32HFPJVv6S+sFA9TmBIBDaqNlFPmBH58ZjgNnGhEr/4oBZvGr4q4TjQ== + version "3.1.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ethers/-/hardhat-ethers-3.1.0.tgz#d33595e85cdb53e802391de64ee35910c078dbb0" + integrity sha512-jx6fw3Ms7QBwFGT2MU6ICG292z0P81u6g54JjSV105+FbTZOF4FJqPksLfDybxkkOeq28eDxbqq7vpxRYyIlxA== dependencies: debug "^4.1.1" lodash.isequal "^4.5.0" "@nomicfoundation/hardhat-ignition-ethers@^0.15.0": - version "0.15.13" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ignition-ethers/-/hardhat-ignition-ethers-0.15.13.tgz#34be3c13183fad33ef0347d8d7858bf3b19b7599" - integrity sha512-fJuImb0KBbsylTL5M1DdlChIO/GZoms2NUVJhU+AvfhlgB0jzRH+9jSXE9izYPktd8//tdVSC4kJloJPrR+BlA== + version "0.15.14" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ignition-ethers/-/hardhat-ignition-ethers-0.15.14.tgz#383baf0d949d37b3a0e2ee54b1040ecfe8683e0f" + integrity sha512-eq+5n+c1DW18/Xp8/QrHBBvG5QaKUxYF/byol4f1jrnZ1zAy0OrqEa/oaNFWchhpLalX7d7suk/2EL0PbT0CDQ== "@nomicfoundation/hardhat-ignition@^0.15.10": - version "0.15.12" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ignition/-/hardhat-ignition-0.15.12.tgz#99dfbb69b9f930b6ef6b33a39f5bc9736fed75ed" - integrity sha512-T03bSjFy8vWeKGvFsR42vzl4PgmW06i1e/84m2oowZzdO3i9ax3XJhRiH4kC08QXzkdAdUPinx68hQea8Wh6Jw== + version "0.15.13" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-ignition/-/hardhat-ignition-0.15.13.tgz#6f32eb8bbdccd32eb1eb1ccf26cb5e2689330523" + integrity sha512-G4XGPWvxs9DJhZ6PE1wdvKjHkjErWbsETf4c7YxO6GUz+MJGlw+PtgbnCwhL3tQzSq3oD4MB0LGi+sK0polpUA== dependencies: - "@nomicfoundation/ignition-core" "^0.15.12" - "@nomicfoundation/ignition-ui" "^0.15.11" + "@nomicfoundation/ignition-core" "^0.15.13" + "@nomicfoundation/ignition-ui" "^0.15.12" chalk "^4.0.0" debug "^4.3.2" fs-extra "^10.0.0" @@ -726,9 +719,9 @@ prompts "^2.4.2" "@nomicfoundation/hardhat-network-helpers@^1.0.0": - version "1.0.13" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.13.tgz#8385f87464a4ca2f1b3cc38f2f72c56a3eaa79d2" - integrity sha512-ptg0+SH8jnfoYHlR3dKWTNTB43HZSxkuy3OeDk+AufEKQvQ7Ru9LQEbJtLuDTQ4HGRBkhl4oJ9RABsEIbn7Taw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.1.0.tgz#3ce7958ab83eff58c12e14884c9b01cf0da29b53" + integrity sha512-ZS+NulZuR99NUHt2VwcgZvgeD6Y63qrbORNRuKO+lTowJxNVsrJ0zbRx1j5De6G3dOno5pVGvuYSq2QVG0qCYg== dependencies: ethereumjs-util "^7.1.4" @@ -738,9 +731,9 @@ integrity sha512-FnUtUC5PsakCbwiVNsqlXVIWG5JIb5CEZoSXbJUsEBun22Bivx2jhF1/q9iQbzuaGpJKFQyOhemPB2+XlEE6pQ== "@nomicfoundation/hardhat-verify@^2.0.0": - version "2.0.14" - resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.0.14.tgz#ba80918fac840f1165825f2a422a694486f82f6f" - integrity sha512-z3iVF1WYZHzcdMMUuureFpSAfcnlfJbJx3faOnGrOYg6PRTki1Ut9JAuRccnFzMHf1AmTEoSUpWcyvBCoxL5Rg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/@nomicfoundation/hardhat-verify/-/hardhat-verify-2.1.1.tgz#0af5fc4228df860062865fcafb4a01bc0b89f8a3" + integrity sha512-K1plXIS42xSHDJZRkrE2TZikqxp9T4y6jUMUNI/imLgN5uCcEQokmfU0DlyP9zzHncYK92HlT5IWP35UVCLrPw== dependencies: "@ethersproject/abi" "^5.1.2" "@ethersproject/address" "^5.0.2" @@ -752,10 +745,10 @@ table "^6.8.0" undici "^5.14.0" -"@nomicfoundation/ignition-core@^0.15.10", "@nomicfoundation/ignition-core@^0.15.12": - version "0.15.12" - resolved "https://registry.yarnpkg.com/@nomicfoundation/ignition-core/-/ignition-core-0.15.12.tgz#c09c5aa493e54d82bf1737fa3852428185905f90" - integrity sha512-JJdyoyfM5RXaUqv4c2V/8xpuui4uqJbMCvVnEhgo6FMOK6bqj8wGP6hM4gNE5TLug6ZUCdjIB8kFpofl21RycQ== +"@nomicfoundation/ignition-core@^0.15.10", "@nomicfoundation/ignition-core@^0.15.13": + version "0.15.13" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ignition-core/-/ignition-core-0.15.13.tgz#b2320eb7a6acda7ccb7da0fdd89231206c970ffa" + integrity sha512-Z4T1WIbw0EqdsN9RxtnHeQXBi7P/piAmCu8bZmReIdDo/2h06qgKWxjDoNfc9VBFZJ0+Dx79tkgQR3ewxMDcpA== dependencies: "@ethersproject/address" "5.6.1" "@nomicfoundation/solidity-analyzer" "^0.1.1" @@ -767,10 +760,10 @@ lodash "4.17.21" ndjson "2.0.0" -"@nomicfoundation/ignition-ui@^0.15.11": - version "0.15.11" - resolved "https://registry.yarnpkg.com/@nomicfoundation/ignition-ui/-/ignition-ui-0.15.11.tgz#94969984dd6ca1671a21f2338af4735cf319c1b3" - integrity sha512-VPOVl5xqCKhYCyPOQlposx+stjCwqXQ+BCs5lnw/f2YUfgII+G5Ye0JfHiJOfCJGmqyS03WertBslcj9zQg50A== +"@nomicfoundation/ignition-ui@^0.15.12": + version "0.15.12" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ignition-ui/-/ignition-ui-0.15.12.tgz#661f555dec6194439b0957b050996928eaa88a1a" + integrity sha512-nQl8tusvmt1ANoyIj5RQl9tVSEmG0FnNbtwnWbTim+F8JLm4YLHWS0yEgYUZC+BEO3oS0D8r6V8a02JGZJgqiQ== "@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2": version "0.1.2" @@ -1021,9 +1014,9 @@ tslib "^1.9.3" "@solidity-parser/parser@^0.20.1": - version "0.20.1" - resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.20.1.tgz#88efee3e0946a4856ed10355017692db9c259ff4" - integrity sha512-58I2sRpzaQUN+jJmWbHfbWf9AKfzqCI8JAdFB0vbyY+u8tBRcuTt9LxzasvR0LGQpcRv97eyV7l61FQ3Ib7zVw== + version "0.20.2" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.20.2.tgz#e07053488ed60dae1b54f6fe37bb6d2c5fe146a7" + integrity sha512-rbu0bzwNvMcwAjH86hiEAcOeRI2EeK8zCkHDrFykh/Al8mvJeFmjy3UrE7GYQjNwOgbGUUtCn5/k8CB8zIu7QA== "@spruceid/siwe-parser@^2.1.2": version "2.1.2" @@ -1149,12 +1142,12 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/luxon@~3.6.0": - version "3.6.2" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.6.2.tgz#be6536931801f437eafcb9c0f6d6781f72308041" - integrity sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw== +"@types/luxon@~3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.7.1.tgz#ef51b960ff86801e4e2de80c68813a96e529d531" + integrity sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg== -"@types/minimatch@*": +"@types/minimatch@*", "@types/minimatch@^6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-6.0.0.tgz#4d207b1cc941367bdcd195a3a781a7e4fc3b1e03" integrity sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA== @@ -1167,19 +1160,19 @@ integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q== "@types/node-fetch@^2.6.4": - version "2.6.12" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" - integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + version "2.6.13" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee" + integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw== dependencies: "@types/node" "*" - form-data "^4.0.0" + form-data "^4.0.4" -"@types/node@*", "@types/node@>=13.7.0": - version "24.0.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.10.tgz#f65a169779bf0d70203183a1890be7bee8ca2ddb" - integrity sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA== +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^24.5.2": + version "24.5.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.5.2.tgz#52ceb83f50fe0fcfdfbd2a9fab6db2e9e7ef6446" + integrity sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ== dependencies: - undici-types "~7.8.0" + undici-types "~7.12.0" "@types/node@22.7.5": version "22.7.5" @@ -1189,9 +1182,9 @@ undici-types "~6.19.2" "@types/node@^18.11.18": - version "18.19.115" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.115.tgz#cd94caf14472021b4443c99bcd7aac6bb5c4f672" - integrity sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg== + version "18.19.127" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.127.tgz#7c2e47fa79ad7486134700514d4a975c4607f09d" + integrity sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA== dependencies: undici-types "~5.26.4" @@ -1239,10 +1232,15 @@ abbrev@1.0.x: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" integrity sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q== -abitype@1.0.8, abitype@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.8.tgz#3554f28b2e9d6e9f35eb59878193eabd1b9f46ba" - integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== +abitype@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.1.0.tgz#510c5b3f92901877977af5e864841f443bf55406" + integrity sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A== + +abitype@^1.0.9: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.1.1.tgz#b50ed400f8bfca5452eb4033445c309d3e1117c8" + integrity sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q== abort-controller-x@^0.4.0, abort-controller-x@^0.4.3: version "0.4.3" @@ -1299,9 +1297,9 @@ agent-base@6: debug "4" agent-base@^7.1.2: - version "7.1.3" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" - integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== agentkeepalive@^4.2.1: version "4.6.0" @@ -1368,9 +1366,9 @@ ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^3.2.1: version "3.2.1" @@ -1392,9 +1390,9 @@ ansi-styles@^5.0.0: integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== anymatch@~3.1.2: version "3.1.3" @@ -1585,18 +1583,18 @@ aws4@^1.8.0: integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== axios@^1.6.7, axios@^1.8.4: - version "1.10.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" - integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== + version "1.12.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7" + integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw== dependencies: follow-redirects "^1.15.6" - form-data "^4.0.0" + form-data "^4.0.4" proxy-from-env "^1.1.0" b4a@^1.6.4: - version "1.6.7" - resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" - integrity sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg== + version "1.7.3" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.7.3.tgz#24cf7ccda28f5465b66aec2bac69e32809bf112f" + integrity sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q== bagpipe@^0.3.5: version "0.3.5" @@ -1608,24 +1606,26 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -bare-events@^2.2.0, bare-events@^2.5.4: - version "2.5.4" - resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.4.tgz#16143d435e1ed9eafd1ab85f12b89b3357a41745" - integrity sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA== +bare-events@^2.5.4, bare-events@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.7.0.tgz#46596dae9c819c5891eb2dcc8186326ed5a6da54" + integrity sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA== bare-fs@^4.0.1: - version "4.1.6" - resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.1.6.tgz#0925521e7310f65cb1f154cab264f0b647a7cdef" - integrity sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ== + version "4.4.4" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.4.4.tgz#69e7da11d4d7d4a77e1dc9454e11f7ac13a93462" + integrity sha512-Q8yxM1eLhJfuM7KXVP3zjhBvtMJCYRByoTT+wHXjpdMELv0xICFJX+1w4c7csa+WZEOsq4ItJ4RGwvzid6m/dw== dependencies: bare-events "^2.5.4" bare-path "^3.0.0" bare-stream "^2.6.4" + bare-url "^2.2.2" + fast-fifo "^1.3.2" bare-os@^3.0.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.1.tgz#9921f6f59edbe81afa9f56910658422c0f4858d4" - integrity sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g== + version "3.6.2" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.2.tgz#b3c4f5ad5e322c0fd0f3c29fc97d19009e2796e5" + integrity sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A== bare-path@^3.0.0: version "3.0.0" @@ -1635,12 +1635,19 @@ bare-path@^3.0.0: bare-os "^3.0.1" bare-stream@^2.6.4: - version "2.6.5" - resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.6.5.tgz#bba8e879674c4c27f7e27805df005c15d7a2ca07" - integrity sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA== + version "2.7.0" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.7.0.tgz#5b9e7dd0a354d06e82d6460c426728536c35d789" + integrity sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A== dependencies: streamx "^2.21.0" +bare-url@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/bare-url/-/bare-url-2.2.2.tgz#1369d1972bbd7d9b358d0214d3f12abe2328d3e9" + integrity sha512-g+ueNGKkrjMazDG3elZO1pNs3HY5+mMmOet1jtKyhOaCnkLzitxf26z7hoAEkDNgdNmnc1KIlt/dw6Po6xZMpA== + dependencies: + bare-path "^3.0.0" + base-x@^3.0.2: version "3.0.11" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" @@ -1675,9 +1682,9 @@ better-queue@^3.8.12: uuid "^9.0.0" bignumber.js@^9.0.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd" - integrity sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA== + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== binary-extensions@^2.0.0, binary-extensions@^2.2.0: version "2.3.0" @@ -1996,12 +2003,13 @@ ci-info@^2.0.0: integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.6.tgz#8fe672437d01cd6c4561af5334e0cc50ff1955f7" - integrity sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.7.tgz#bd094bfef42634ccfd9e13b9fc73274997111e39" + integrity sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA== dependencies: inherits "^2.0.4" safe-buffer "^5.2.1" + to-buffer "^1.2.2" clean-stack@^2.0.0: version "2.2.0" @@ -2234,16 +2242,6 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hash@~1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" - integrity sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - ripemd160 "^2.0.0" - sha.js "^2.4.0" - create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" @@ -2262,12 +2260,12 @@ create-require@^1.1.0: integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cron@^4.1.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/cron/-/cron-4.3.1.tgz#8dcc980864d3163bae3fc607be6e39707b7d2b97" - integrity sha512-7x7DoEOxV11t3OPWWMjj1xrL1PGkTV5RV+/54IJTZD7gStiaMploY43EkeBSkDZTLRbUwk+OISbQ0TR133oXyA== + version "4.3.3" + resolved "https://registry.yarnpkg.com/cron/-/cron-4.3.3.tgz#d37cfcbc73ba34a50d9d9ce9b653ae60837377d7" + integrity sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw== dependencies: - "@types/luxon" "~3.6.0" - luxon "~3.6.0" + "@types/luxon" "~3.7.0" + luxon "~3.7.0" cross-fetch@^3.1.5: version "3.2.0" @@ -2361,9 +2359,9 @@ debug@2.6.9: ms "2.0.0" debug@4, debug@^4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: - version "4.4.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" - integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" @@ -2741,9 +2739,9 @@ escodegen@1.8.x: source-map "~0.2.0" eslint-config-prettier@^10.0.2: - version "10.1.5" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782" - integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw== + version "10.1.8" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" + integrity sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w== eslint-scope@^8.4.0: version "8.4.0" @@ -2764,18 +2762,18 @@ eslint-visitor-keys@^4.2.1: integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== eslint@^9.21.0: - version "9.30.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.30.1.tgz#d4107b39964412acd9b5c0744f1c6df514fa1211" - integrity sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ== + version "9.36.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.36.0.tgz#9cc5cbbfb9c01070425d9bfed81b4e79a1c09088" + integrity sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ== dependencies: - "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.1" "@eslint/config-array" "^0.21.0" - "@eslint/config-helpers" "^0.3.0" - "@eslint/core" "^0.14.0" + "@eslint/config-helpers" "^0.3.1" + "@eslint/core" "^0.15.2" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.30.1" - "@eslint/plugin-kit" "^0.3.1" + "@eslint/js" "9.36.0" + "@eslint/plugin-kit" "^0.3.5" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -2957,6 +2955,13 @@ eventemitter3@^4.0.4: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events-universal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/events-universal/-/events-universal-1.0.1.tgz#b56a84fd611b6610e0a2d0f09f80fdf931e2dfe6" + integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw== + dependencies: + bare-events "^2.7.0" + events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -2981,15 +2986,15 @@ express-rate-limit@^7.5.0: integrity sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw== express-session@^1.17.3: - version "1.18.1" - resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.1.tgz#88d0bbd41878882840f24ec6227493fcb167e8d5" - integrity sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA== + version "1.18.2" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.18.2.tgz#34db6252611b57055e877036eea09b4453dec5d8" + integrity sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A== dependencies: cookie "0.7.2" cookie-signature "1.0.7" debug "2.6.9" depd "~2.0.0" - on-headers "~1.0.2" + on-headers "~1.1.0" parseurl "~1.3.3" safe-buffer "5.2.1" uid-safe "~2.1.5" @@ -3078,9 +3083,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastq@^1.6.0: version "1.19.1" @@ -3089,10 +3094,10 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fdir@^6.4.4: - version "6.4.6" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.6.tgz#2b268c0232697063111bbf3f64810a2a741ba281" - integrity sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w== +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== fecha@^4.2.0: version "4.2.3" @@ -3170,9 +3175,9 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.12.1, follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== for-each@^0.3.3, for-each@^0.3.5: version "0.3.5" @@ -3199,10 +3204,10 @@ form-data-encoder@1.7.2: resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== -form-data@^4.0.0, form-data@~4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.3.tgz#608b1b3f3e28be0fccf5901fc85fb3641e5cf0ae" - integrity sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA== +form-data@^4.0.4, form-data@~4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -3479,9 +3484,9 @@ globals@^14.0.0: integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globals@^16.0.0: - version "16.3.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-16.3.0.tgz#66118e765ddaf9e2d880f7e17658543f93f1f667" - integrity sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ== + version "16.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-16.4.0.tgz#574bc7e72993d40cf27cf6c241f324ee77808e51" + integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw== globalthis@^1.0.4: version "1.0.4" @@ -3540,7 +3545,7 @@ graphql-request@^6.1.0: "@graphql-typed-document-node/core" "^3.2.0" cross-fetch "^3.1.5" -graphql@^16.10.0: +graphql@^16.11.0: version "16.11.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.11.0.tgz#96d17f66370678027fdf59b2d4c20b4efaa8a633" integrity sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw== @@ -3596,9 +3601,9 @@ hardhat-gas-reporter@^2.2.2: viem "^2.27.0" hardhat@^2.24.1: - version "2.26.1" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.26.1.tgz#c5f7418be0cba5224c6ac42eae3e24db113e55f6" - integrity sha512-CXWuUaTtehxiHPCdlitntctfeYRgujmXkNX5gnrD5jdA6HhRQt+WWBZE/gHXbE29y/wDmmUL2d652rI0ctjqjw== + version "2.26.3" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.26.3.tgz#87f3f4b6d1001970299d5bff135d57e8adae7a07" + integrity sha512-gBfjbxCCEaRgMCRgTpjo1CEoJwqNPhyGMMVHYZJxoQ3LLftp2erSVf8ZF6hTQC0r2wst4NcqNmLWqMnHg1quTw== dependencies: "@ethereumjs/util" "^9.1.0" "@ethersproject/abi" "^5.1.2" @@ -3686,21 +3691,15 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -hash-base@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" - integrity sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw== - dependencies: - inherits "^2.0.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== +hash-base@^3.0.0, hash-base@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.2.tgz#79d72def7611c3f6e3c3b5730652638001b10a74" + integrity sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg== dependencies: inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + to-buffer "^1.2.1" hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" @@ -3940,9 +3939,9 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: get-intrinsic "^1.2.6" is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + version "0.3.4" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" + integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA== is-async-function@^2.0.0: version "2.1.1" @@ -4199,9 +4198,9 @@ js-sha3@0.8.0, js-sha3@^0.8.0: integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== js-tiktoken@^1.0.12: - version "1.0.20" - resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.20.tgz#fa2733bf147acaf1bdcf9ab8a878e79c581c95f2" - integrity sha512-Xlaqhhs8VfCd6Sh7a1cFkZHQbYTLCwVJJWiHVxBYzLPxW0XsoxBy1hitmjkdIjD3Aon5BXLHFwU5O8WUx6HH+A== + version "1.0.21" + resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.21.tgz#368a9957591a30a62997dd0c4cf30866f00f8221" + integrity sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g== dependencies: base64-js "^1.5.1" @@ -4280,9 +4279,9 @@ jsonfile@^4.0.0: graceful-fs "^4.1.6" jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== dependencies: universalify "^2.0.0" optionalDependencies: @@ -4364,16 +4363,16 @@ kuler@^2.0.0: integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== "langchain@>=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", langchain@^0.3.19: - version "0.3.29" - resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.3.29.tgz#74a50cdce9dc921362933f34a0e4bf53b0b1ceeb" - integrity sha512-L389pKlApVJPqu4hp58qY6NZAobI+MFPoBjSfjT1z3mcxtB68wLFGhaH4DVsTVg21NYO+0wTEoz24BWrxu9YGw== + version "0.3.34" + resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.3.34.tgz#b888c22275b75699d4b17c4475f4b67949cb5572" + integrity sha512-OADHLQYRX+36EqQBxIoryCdMKfHex32cJBSWveadIIeRhygqivacIIDNwVjX51Y++c80JIdR0jaQHWn2r3H1iA== dependencies: - "@langchain/openai" ">=0.1.0 <0.6.0" + "@langchain/openai" ">=0.1.0 <0.7.0" "@langchain/textsplitters" ">=0.0.0 <0.2.0" js-tiktoken "^1.0.12" js-yaml "^4.1.0" jsonpointer "^5.0.1" - langsmith "^0.3.33" + langsmith "^0.3.67" openapi-types "^12.1.3" p-retry "4" uuid "^10.0.0" @@ -4392,10 +4391,10 @@ langsmith@^0.1.43: semver "^7.6.3" uuid "^10.0.0" -langsmith@^0.3.33: - version "0.3.39" - resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.39.tgz#668918ad6a19e9772c7bbc551c03867ea48f07d0" - integrity sha512-dzrsERzrDh/jSuhOACcZaZf3zENSROk+1XGWFdBxeBa/aFuV7sQzG+wYH+Pdr+VOw2RUoFzl+PHzkuWZw1zuRQ== +langsmith@^0.3.67: + version "0.3.69" + resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.3.69.tgz#ae9f3db6e11cac8c337401ead5d05171002fdc54" + integrity sha512-YKzu92YAP2o+d+1VmR38xqFX0RIRLKYj1IqdflVEY83X0FoiVlrWO3xDLXgnu7vhZ2N2M6jx8VO9fVF8yy9gHA== dependencies: "@types/uuid" "^10.0.0" chalk "^4.1.2" @@ -4517,7 +4516,7 @@ logform@^2.7.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" -long@^5.0.0, long@^5.2.4: +long@^5.0.0, long@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== @@ -4539,10 +4538,10 @@ lru_map@^0.3.3: resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== -luxon@~3.6.0: - version "3.6.1" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.6.1.tgz#d283ffc4c0076cb0db7885ec6da1c49ba97e47b0" - integrity sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ== +luxon@~3.7.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" + integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== mailparser@^3.7.2: version "3.7.4" @@ -4676,7 +4675,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@*: +minimatch@*, minimatch@^10.0.0: version "10.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.3.tgz#cf7a0314a16c4d9ab73a7730a0e8e3c3502d47aa" integrity sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw== @@ -4775,9 +4774,9 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== multer@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/multer/-/multer-2.0.1.tgz#3ed335ed2b96240e3df9e23780c91cfcf5d29202" - integrity sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ== + version "2.0.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-2.0.2.tgz#08a8aa8255865388c387aaf041426b0c87bf58dd" + integrity sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw== dependencies: append-field "^1.0.0" busboy "^1.6.0" @@ -4793,9 +4792,9 @@ mustache@^4.2.0: integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== nanoid@^5.0.0: - version "5.1.5" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.5.tgz#f7597f9d9054eb4da9548cdd53ca70f1790e87de" - integrity sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw== + version "5.1.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.6.tgz#30363f664797e7d40429f6c16946d6bd7a3f26c9" + integrity sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg== natural-compare@^1.4.0: version "1.4.0" @@ -4823,7 +4822,7 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -nice-grpc-client-middleware-retry@^3.1.10: +nice-grpc-client-middleware-retry@^3.1.11: version "3.1.11" resolved "https://registry.yarnpkg.com/nice-grpc-client-middleware-retry/-/nice-grpc-client-middleware-retry-3.1.11.tgz#4fc0128b891d184b6c98af3bfd6aca1b608a3fd1" integrity sha512-xW/imz/kNG2g0DwTfH2eYEGrg1chSLrXtvGp9fg2qkhTgGFfAS/Pq3+t+9G8KThcC4hK/xlEyKvZWKk++33S6A== @@ -4838,7 +4837,7 @@ nice-grpc-common@^2.0.2: dependencies: ts-error "^1.0.6" -nice-grpc@^2.1.11: +nice-grpc@^2.1.12: version "2.1.12" resolved "https://registry.yarnpkg.com/nice-grpc/-/nice-grpc-2.1.12.tgz#56ffdcc4d5bc3d0271c176210680c4bd10c5149b" integrity sha512-J1n4Wg+D3IhRhGQb+iqh2OpiM0GzTve/kf2lnlW4S+xczmIEd0aHUDV1OsJ5a3q8GSTqJf+s4Rgg1M8uJltarw== @@ -4991,10 +4990,10 @@ obliterator@^2.0.0: resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.5.tgz#031e0145354b0c18840336ae51d41e7d6d2c76aa" integrity sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw== -ollama@^0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.16.tgz#cb695b4aab6f6c07236a04b3aee40160f4f9e892" - integrity sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w== +ollama@^0.5.17: + version "0.5.18" + resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.18.tgz#10b8ee9e5cd840f2003b7bbea1802dd772e6a564" + integrity sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg== dependencies: whatwg-fetch "^3.6.20" @@ -5005,10 +5004,10 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" @@ -5024,6 +5023,11 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +openai@5.12.2: + version "5.12.2" + resolved "https://registry.yarnpkg.com/openai/-/openai-5.12.2.tgz#512ab6b80eb8414837436e208f1b951442b97761" + integrity sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ== + openai@^4.102.0: version "4.104.0" resolved "https://registry.yarnpkg.com/openai/-/openai-4.104.0.tgz#c489765dc051b95019845dab64b0e5207cae4d30" @@ -5037,11 +5041,6 @@ openai@^4.102.0: formdata-node "^4.3.2" node-fetch "^2.6.7" -openai@^5.3.0: - version "5.8.2" - resolved "https://registry.yarnpkg.com/openai/-/openai-5.8.2.tgz#b12f968618deb87f6231b35a21b590d0b529d70b" - integrity sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q== - openapi-types@^12.1.3: version "12.1.3" resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" @@ -5090,18 +5089,18 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" -ox@0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/ox/-/ox-0.8.1.tgz#c1328e4c890583b9c19d338126aef4b796d53543" - integrity sha512-e+z5epnzV+Zuz91YYujecW8cF01mzmrUtWotJ0oEPym/G82uccs7q0WDHTYL3eiONbTUEvcZrptAKLgTBD3u2A== +ox@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.9.6.tgz#5cf02523b6db364c10ee7f293ff1e664e0e1eab7" + integrity sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg== dependencies: "@adraffy/ens-normalize" "^1.11.0" "@noble/ciphers" "^1.3.0" - "@noble/curves" "^1.9.1" + "@noble/curves" "1.9.1" "@noble/hashes" "^1.8.0" "@scure/bip32" "^1.7.0" "@scure/bip39" "^1.6.0" - abitype "^1.0.8" + abitype "^1.0.9" eventemitter3 "5.0.1" p-finally@^1.0.0: @@ -5227,16 +5226,16 @@ pathval@^1.1.1: integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== pbkdf2@^3.0.17, pbkdf2@^3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.3.tgz#8be674d591d65658113424592a95d1517318dd4b" - integrity sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA== + version "3.1.5" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.5.tgz#444a59d7a259a95536c56e80c89de31cc01ed366" + integrity sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ== dependencies: - create-hash "~1.1.3" + create-hash "^1.2.0" create-hmac "^1.1.7" - ripemd160 "=2.0.1" + ripemd160 "^2.0.3" safe-buffer "^5.2.1" - sha.js "^2.4.11" - to-buffer "^1.2.0" + sha.js "^2.4.12" + to-buffer "^1.2.1" peberminta@^0.9.0: version "0.9.0" @@ -5314,10 +5313,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" - integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== pify@^4.0.1: version "4.0.1" @@ -5389,10 +5388,10 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -protobufjs@^7.2.5: - version "7.5.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.3.tgz#13f95a9e3c84669995ec3652db2ac2fb00b89363" - integrity sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw== +protobufjs@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -5528,7 +5527,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.5, readable-stream@^2.3.5: +readable-stream@^2.0.5, readable-stream@^2.3.5, readable-stream@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -5684,21 +5683,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== -ripemd160@=2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" - integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w== +ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.3.tgz#9be54e4ba5e3559c8eee06a25cd7648bbccdf5a8" + integrity sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA== dependencies: - hash-base "^2.0.0" - inherits "^2.0.1" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" + hash-base "^3.1.2" + inherits "^2.0.4" rlp@^2.2.4: version "2.2.7" @@ -5730,7 +5721,7 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5919,7 +5910,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: +sha.js@^2.4.0, sha.js@^2.4.12, sha.js@^2.4.8: version "2.4.12" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== @@ -6008,9 +5999,9 @@ signal-exit@^4.0.1: integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + version "0.2.4" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667" + integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw== dependencies: is-arrayish "^0.3.1" @@ -6186,14 +6177,13 @@ streamsearch@^1.1.0: integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== streamx@^2.15.0, streamx@^2.21.0: - version "2.22.1" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.22.1.tgz#c97cbb0ce18da4f4db5a971dc9ab68ff5dc7f5a5" - integrity sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA== + version "2.23.0" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.23.0.tgz#7d0f3d00d4a6c5de5728aecd6422b4008d66fd0b" + integrity sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg== dependencies: + events-universal "^1.0.0" fast-fifo "^1.3.2" text-decoder "^1.1.0" - optionalDependencies: - bare-events "^2.2.0" string-format@^2.0.0: version "2.0.0" @@ -6293,9 +6283,9 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: ansi-regex "^5.0.1" strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== dependencies: ansi-regex "^6.0.1" @@ -6366,9 +6356,9 @@ table@^6.8.0: strip-ansi "^6.0.1" tar-fs@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.0.tgz#4675e2254d81410e609d91581a762608de999d25" - integrity sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w== + version "3.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.1.tgz#4f164e59fb60f103d472360731e8c6bb4a7fe9ef" + integrity sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg== dependencies: pump "^3.0.0" tar-stream "^3.1.5" @@ -6419,12 +6409,12 @@ through2@^4.0.0: readable-stream "3" tinyglobby@^0.2.6: - version "0.2.14" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" - integrity sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ== + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== dependencies: - fdir "^6.4.4" - picomatch "^4.0.2" + fdir "^6.5.0" + picomatch "^4.0.3" tlds@1.259.0: version "1.259.0" @@ -6450,10 +6440,10 @@ tmp@0.0.33: dependencies: os-tmpdir "~1.0.2" -to-buffer@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.1.tgz#2ce650cdb262e9112a18e65dc29dcb513c8155e0" - integrity sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ== +to-buffer@^1.2.0, to-buffer@^1.2.1, to-buffer@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" + integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== dependencies: isarray "^2.0.5" safe-buffer "^5.2.1" @@ -6695,9 +6685,9 @@ typedarray@^0.0.6: integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== typescript@>=4.5.0: - version "5.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" - integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + version "5.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== typical@^4.0.0: version "4.0.0" @@ -6751,10 +6741,10 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== -undici-types@~7.8.0: - version "7.8.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294" - integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw== +undici-types@~7.12.0: + version "7.12.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.12.0.tgz#15c5c7475c2a3ba30659529f5cdb4674b622fafb" + integrity sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ== undici@^5.14.0: version "5.29.0" @@ -6860,30 +6850,30 @@ verror@1.10.0: extsprintf "^1.2.0" viem@^2.23.15, viem@^2.27.0: - version "2.31.7" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.31.7.tgz#1b8afa221a96a98edf9349760c6925f67c123dd6" - integrity sha512-mpB8Hp6xK77E/b/yJmpAIQcxcOfpbrwWNItjnXaIA8lxZYt4JS433Pge2gg6Hp3PwyFtaUMh01j5L8EXnLTjQQ== + version "2.37.8" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.37.8.tgz#5290a041c9bfbf9ea92e6348d65bb05e928c2ee0" + integrity sha512-mL+5yvCQbRIR6QvngDQMfEiZTfNWfd+/QL5yFaOoYbpH3b1Q2ddwF7YG2eI2AcYSh9LE1gtUkbzZLFUAVyj4oQ== dependencies: - "@noble/curves" "1.9.2" + "@noble/curves" "1.9.1" "@noble/hashes" "1.8.0" "@scure/bip32" "1.7.0" "@scure/bip39" "1.6.0" - abitype "1.0.8" + abitype "1.1.0" isows "1.0.7" - ox "0.8.1" - ws "8.18.2" + ox "0.9.6" + ws "8.18.3" weaviate-client@^3.5.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/weaviate-client/-/weaviate-client-3.6.2.tgz#f2e6f96690fcd5f0c601afc401a662062f8bc8bd" - integrity sha512-6z+Du0Sp+nVp4Mhsn25sd+Qw6fr60vbyUS1e3gTZqtMrxLuNC1xgA0J/MHu5oHcm6moCBqT/2AQCt4ZV4fYSaw== + version "3.9.0" + resolved "https://registry.yarnpkg.com/weaviate-client/-/weaviate-client-3.9.0.tgz#e597c596b40cf5bf4be78ca19d543d338d6a8535" + integrity sha512-7qwg7YONAaT4zWnohLrFdzky+rZegVe76J+Tky/+7tuyvjFpdKgSrdqI/wPDh8aji0ZGZrL4DdGwGfFnZ+uV4w== dependencies: abort-controller-x "^0.4.3" - graphql "^16.10.0" + graphql "^16.11.0" graphql-request "^6.1.0" - long "^5.2.4" - nice-grpc "^2.1.11" - nice-grpc-client-middleware-retry "^3.1.10" + long "^5.3.2" + nice-grpc "^2.1.12" + nice-grpc-client-middleware-retry "^3.1.11" nice-grpc-common "^2.0.2" uuid "^9.0.1" @@ -7089,7 +7079,7 @@ write-file-atomic@3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@8.17.1, ws@8.18.2, ws@^7.4.6, ws@^8.18.0, ws@^8.18.1: +ws@8.17.1, ws@8.18.3, ws@^7.4.6, ws@^8.18.0, ws@^8.18.1: version "8.18.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== @@ -7105,9 +7095,9 @@ y18n@^5.0.5: integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yaml@^2.2.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.0.tgz#15f8c9866211bdc2d3781a0890e44d4fa1a5fff6" - integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== + version "2.8.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" @@ -7174,12 +7164,12 @@ zip-stream@^6.0.1: compress-commons "^6.0.2" readable-stream "^4.0.0" -zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4: +zod-to-json-schema@^3.22.3: version "3.24.6" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d" integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg== zod@^3.22.4, zod@^3.25.32: - version "3.25.74" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.74.tgz#9368a3986fe756bd94b9a5baad9919660ff3f250" - integrity sha512-J8poo92VuhKjNknViHRAIuuN6li/EwFbAC8OedzI8uxpEPGiXHGQu9wemIAioIpqgfB4SySaJhdk0mH5Y4ICBg== + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== diff --git a/docker-compose.yml b/docker-compose.yml index f6a2998..021c22e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,8 +20,8 @@ services: POSTGRES_DB: ${DB_NAME:-dapp_db} POSTGRES_USER: ${DB_USER:-dapp_user} POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password} - # ports: - # - '5432:5432' # Закрываем доступ к базе данных извне + ports: + - '5432:5432' # Открываем доступ к базе данных извне для разработки healthcheck: test: - CMD-SHELL diff --git a/docs/DLE_DEPLOY_GUIDE.md b/docs/DLE_DEPLOY_GUIDE.md index 370f19f..218a8ed 100644 --- a/docs/DLE_DEPLOY_GUIDE.md +++ b/docs/DLE_DEPLOY_GUIDE.md @@ -14,7 +14,7 @@ ## Обзор -DLE v2 (Digital Legal Entity) - это система для создания цифровых юридических лиц с мульти-чейн поддержкой. Основная особенность - использование CREATE2 для обеспечения одинакового адреса смарт-контракта во всех поддерживаемых сетях. +DLE v2 (Digital Legal Entity) - это система для создания цифровых юридических лиц с мульти-чейн поддержкой. Основная особенность - использование CREATE с выровненным nonce для обеспечения одинакового адреса смарт-контракта во всех поддерживаемых сетях. ## Архитектура @@ -26,7 +26,7 @@ DLE v2 (Digital Legal Entity) - это система для создания ц ### Мульти-чейн поддержка -- **CREATE2** - Одинаковый адрес во всех EVM-совместимых сетях +- **CREATE с выровненным nonce** - Одинаковый адрес во всех EVM-совместимых сетях - **Single-Chain Governance** - Голосование происходит в одной сети - **Multi-Chain Execution** - Исполнение в целевых сетях по подписям @@ -146,33 +146,42 @@ CREATE TABLE factory_addresses ( ); ``` -### CREATE2 Механизм +### CREATE Механизм -Система использует двухуровневый CREATE2 для обеспечения одинаковых адресов: +Система использует CREATE с выровненным nonce для обеспечения одинаковых адресов: -#### 1. Factory Deployer -```solidity -// Предсказуемый адрес Factory через CREATE -address factoryAddress = getCreateAddress( - from: deployerAddress, - nonce: deployerNonce -); +#### 1. Выравнивание nonce +```javascript +// Выравнивание nonce до целевого значения +while (currentNonce < targetNonce) { + await sendTransaction({ + to: burnAddress, + value: 0, + nonce: currentNonce + }); + currentNonce++; +} ``` -#### 2. DLE Contract -```solidity -// Вычисление адреса DLE через CREATE2 -address predictedAddress = factoryDeployer.computeAddress( - salt, - keccak256(creationCode) -); +#### 2. Деплой DLE +```javascript +// Вычисление адреса DLE через CREATE +const predictedAddress = ethers.getCreateAddress({ + from: wallet.address, + nonce: targetNonce +}); -// Деплой DLE с одинаковым адресом -factoryDeployer.deploy(salt, creationCode); +// Деплой DLE с предсказанным адресом +await wallet.sendTransaction({ + data: dleInitCode, + nonce: targetNonce +}); ``` #### Ключевые принципы: -- **Factory Deployer** деплоится с одинаковым адресом во всех сетях +- **Выровненный nonce** обеспечивает одинаковые адреса во всех сетях +- **Burn address** используется для выравнивания nonce без потери средств +- **Проверка баланса** перед деплоем предотвращает неудачи - **DLE Contract** деплоится через Factory с одинаковым salt - **Результат**: Одинаковый адрес DLE во всех EVM-совместимых сетях diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 50df35a..a5f8095 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -53,6 +53,7 @@ import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits, provi import { useAuthContext } from '../composables/useAuth'; import { useAuthFlow } from '../composables/useAuthFlow'; import { useNotifications } from '../composables/useNotifications'; +import { useTokenBalancesWebSocket } from '../composables/useTokenBalancesWebSocket'; import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage'; import { connectWithWallet } from '../services/wallet'; import api from '../api/axios'; @@ -68,7 +69,10 @@ import NotificationDisplay from './NotificationDisplay.vue'; const auth = useAuthContext(); const { notifications, showSuccessMessage, showErrorMessage } = useNotifications(); -// Определяем props, которые будут приходить от родительского View +// Используем useTokenBalancesWebSocket для получения актуального состояния загрузки +const { isLoadingTokens: wsIsLoadingTokens } = useTokenBalancesWebSocket(); + +// Определяем props, которые будут приходить от родительского View (оставляем для совместимости) const props = defineProps({ isAuthenticated: Boolean, identities: Array, @@ -79,17 +83,26 @@ const props = defineProps({ // Определяем emits const emit = defineEmits(['auth-action-completed']); +// Используем useAuth напрямую для получения актуальных данных +const isAuthenticated = computed(() => auth.isAuthenticated.value); +const identities = computed(() => auth.identities.value); +const tokenBalances = computed(() => auth.tokenBalances.value); +const isLoadingTokens = computed(() => { + // Приоритет: WebSocket состояние > пропс > false + return wsIsLoadingTokens.value || (props.isLoadingTokens !== undefined ? props.isLoadingTokens : false); +}); + // Предоставляем данные дочерним компонентам через provide/inject -provide('isAuthenticated', computed(() => props.isAuthenticated)); -provide('identities', computed(() => props.identities)); -provide('tokenBalances', computed(() => props.tokenBalances)); -provide('isLoadingTokens', computed(() => props.isLoadingTokens)); +provide('isAuthenticated', isAuthenticated); +provide('identities', identities); +provide('tokenBalances', tokenBalances); +provide('isLoadingTokens', isLoadingTokens); // Отладочная информация -console.log('[BaseLayout] Props received:', { - isAuthenticated: props.isAuthenticated, - tokenBalances: props.tokenBalances, - isLoadingTokens: props.isLoadingTokens +console.log('[BaseLayout] Auth state:', { + isAuthenticated: isAuthenticated.value, + tokenBalances: tokenBalances.value, + isLoadingTokens: isLoadingTokens.value }); // Callback после успешной аутентификации/привязки через Email/Telegram @@ -168,6 +181,12 @@ const handleWalletAuth = async () => { errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.'; } else if (error.message && error.message.includes('Браузерный кошелек не установлен')) { errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.'; + } else if (error.message && error.message.includes('Не удалось получить nonce')) { + errorMessage = 'Ошибка получения nonce. Попробуйте обновить страницу и повторить попытку.'; + } else if (error.message && error.message.includes('Invalid nonce')) { + errorMessage = 'Ошибка аутентификации. Попробуйте обновить страницу и повторить попытку.'; + } else if (error.message && error.message.includes('Nonce expired')) { + errorMessage = 'Время сессии истекло. Попробуйте обновить страницу и повторить попытку.'; } else if (error.message) { errorMessage = error.message; } diff --git a/frontend/src/components/NetworkSwitchNotification.vue b/frontend/src/components/NetworkSwitchNotification.vue new file mode 100644 index 0000000..9327e86 --- /dev/null +++ b/frontend/src/components/NetworkSwitchNotification.vue @@ -0,0 +1,223 @@ + + + + + + + diff --git a/frontend/src/components/WebSshForm.vue b/frontend/src/components/WebSshForm.vue index c37617b..c6b9d9f 100644 --- a/frontend/src/components/WebSshForm.vue +++ b/frontend/src/components/WebSshForm.vue @@ -192,5 +192,275 @@ const formatTime = (timestamp) => { \ No newline at end of file diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index a258e83..4d3a2f4 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -30,6 +30,12 @@ const userAccessLevel = ref({ level: 'user', tokenCount: 0, hasAccess: false }); const updateIdentities = async () => { if (!isAuthenticated.value || !userId.value) return; + // Проверяем, что identities ref существует + if (!identities || typeof identities.value === 'undefined') { + console.warn('Identities ref is not initialized'); + return; + } + try { const response = await axios.get('/auth/identities'); if (response.data.success) { @@ -46,14 +52,26 @@ const updateIdentities = async () => { }, []); // Сравниваем новый отфильтрованный список с текущим значением - const currentProviders = identities.value.map(id => id.provider).sort(); - const newProviders = filteredIdentities.map(id => id.provider).sort(); + const currentProviders = (identities.value || []).map(id => id?.provider || '').sort(); + const newProviders = (filteredIdentities || []).map(id => id?.provider || '').sort(); const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders); - // Обновляем реактивное значение - identities.value = filteredIdentities; - console.log('User identities updated:', identities.value); + // Обновляем реактивное значение с проверкой + try { + if (identities && identities.value !== undefined) { + identities.value = filteredIdentities; + console.log('User identities updated:', identities.value); + } else { + console.warn('Identities ref is not available or not initialized'); + } + } catch (error) { + console.error('Error updating identities:', error); + // Если произошла ошибка, пытаемся инициализировать identities + if (identities && typeof identities.value === 'undefined') { + identities.value = []; + } + } // Если список идентификаторов изменился, принудительно проверяем аутентификацию, // чтобы обновить authType и другие связанные данные (например, telegramId) @@ -163,11 +181,21 @@ const updateAuth = async ({ // Обновляем идентификаторы при любом изменении аутентификации if (authenticated) { - await updateIdentities(); - startIdentitiesPolling(); + try { + await updateIdentities(); + startIdentitiesPolling(); + } catch (error) { + console.error('Error updating identities in updateAuth:', error); + } } else { stopIdentitiesPolling(); - identities.value = []; + try { + if (identities && typeof identities.value !== 'undefined') { + identities.value = []; + } + } catch (error) { + console.error('Error clearing identities:', error); + } } console.log('Auth updated:', { @@ -306,7 +334,11 @@ const checkAuth = async () => { // Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения if (response.data.authenticated) { // Сначала обновляем идентификаторы, чтобы иметь актуальные данные - await updateIdentities(); + try { + await updateIdentities(); + } catch (error) { + console.error('Error updating identities in checkAuth:', error); + } // Если пользователь только что аутентифицировался или сменил аккаунт, // связываем гостевые сообщения с его аккаунтом diff --git a/frontend/src/composables/useDleContract.js b/frontend/src/composables/useDleContract.js new file mode 100644 index 0000000..a10fd4d --- /dev/null +++ b/frontend/src/composables/useDleContract.js @@ -0,0 +1,349 @@ +import { ref, computed } from 'vue'; +import { ethers } from 'ethers'; +import { DLE_ABI, TOKEN_ABI } from '@/utils/dle-abi'; + +/** + * Композабл для работы с DLE смарт-контрактом + * Содержит правильные ABI и функции для взаимодействия с контрактом + */ +export function useDleContract() { + // Состояние + const isConnected = ref(false); + const provider = ref(null); + const signer = ref(null); + const contract = ref(null); + const userAddress = ref(null); + const chainId = ref(null); + + // Используем общий ABI из utils/dle-abi.js + + /** + * Подключиться к кошельку + */ + const connectWallet = async () => { + try { + if (!window.ethereum) { + throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.'); + } + + // Запрашиваем подключение + const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); + + // Создаем провайдер и подписанта + provider.value = new ethers.BrowserProvider(window.ethereum); + signer.value = await provider.value.getSigner(); + userAddress.value = await signer.value.getAddress(); + + // Получаем информацию о сети + const network = await provider.value.getNetwork(); + chainId.value = Number(network.chainId); + + isConnected.value = true; + + console.log('✅ Кошелек подключен:', { + address: userAddress.value, + chainId: chainId.value, + network: network.name + }); + + return { + success: true, + address: userAddress.value, + chainId: chainId.value + }; + } catch (error) { + console.error('❌ Ошибка подключения к кошельку:', error); + isConnected.value = false; + throw error; + } + }; + + /** + * Инициализировать контракт + */ + const initContract = (contractAddress) => { + if (!provider.value) { + throw new Error('Провайдер не инициализирован. Сначала подключите кошелек.'); + } + + contract.value = new ethers.Contract(contractAddress, DLE_ABI, signer.value); + console.log('✅ DLE контракт инициализирован:', contractAddress); + }; + + /** + * Проверить баланс токенов пользователя + */ + const checkTokenBalance = async (contractAddress) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + const balance = await contract.value.balanceOf(userAddress.value); + const balanceFormatted = ethers.formatEther(balance); + + console.log(`💰 Баланс токенов: ${balanceFormatted}`); + + return { + success: true, + balance: balanceFormatted, + hasTokens: balance > 0 + }; + } catch (error) { + console.error('❌ Ошибка проверки баланса:', error); + return { + success: false, + error: error.message, + balance: '0', + hasTokens: false + }; + } + }; + + /** + * Голосовать за предложение + */ + const voteOnProposal = async (contractAddress, proposalId, support) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + console.log('🗳️ Начинаем голосование:', { proposalId, support }); + + // Проверяем баланс токенов + const balanceCheck = await checkTokenBalance(contractAddress); + if (!balanceCheck.hasTokens) { + throw new Error('У вас нет токенов для голосования'); + } + + // Отправляем транзакцию голосования + const tx = await contract.value.vote(proposalId, support); + console.log('📤 Транзакция отправлена:', tx.hash); + + // Ждем подтверждения + const receipt = await tx.wait(); + console.log('✅ Голосование успешно:', receipt); + + return { + success: true, + transactionHash: tx.hash, + receipt: receipt + }; + } catch (error) { + console.error('❌ Ошибка голосования:', error); + + // Улучшенная обработка ошибок + let errorMessage = error.message; + if (error.message.includes('execution reverted')) { + errorMessage = 'Транзакция отклонена смарт-контрактом. Возможные причины:\n' + + '• Предложение уже не активно\n' + + '• Вы уже голосовали за это предложение\n' + + '• Недостаточно прав для голосования\n' + + '• Предложение не существует'; + } else if (error.message.includes('user rejected')) { + errorMessage = 'Транзакция отклонена пользователем'; + } else if (error.message.includes('insufficient funds')) { + errorMessage = 'Недостаточно средств для оплаты газа'; + } + + return { + success: false, + error: errorMessage, + originalError: error + }; + } + }; + + /** + * Исполнить предложение + */ + const executeProposal = async (contractAddress, proposalId) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + console.log('⚡ Исполняем предложение:', proposalId); + + const tx = await contract.value.executeProposal(proposalId); + console.log('📤 Транзакция отправлена:', tx.hash); + + const receipt = await tx.wait(); + console.log('✅ Предложение исполнено:', receipt); + + return { + success: true, + transactionHash: tx.hash, + receipt: receipt + }; + } catch (error) { + console.error('❌ Ошибка исполнения предложения:', error); + return { + success: false, + error: error.message, + originalError: error + }; + } + }; + + /** + * Отменить предложение + */ + const cancelProposal = async (contractAddress, proposalId, reason) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + console.log('❌ Отменяем предложение:', { proposalId, reason }); + + const tx = await contract.value.cancelProposal(proposalId, reason); + console.log('📤 Транзакция отправлена:', tx.hash); + + const receipt = await tx.wait(); + console.log('✅ Предложение отменено:', receipt); + + return { + success: true, + transactionHash: tx.hash, + receipt: receipt + }; + } catch (error) { + console.error('❌ Ошибка отмены предложения:', error); + return { + success: false, + error: error.message, + originalError: error + }; + } + }; + + /** + * Получить состояние предложения + */ + const getProposalState = async (contractAddress, proposalId) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + const state = await contract.value.getProposalState(proposalId); + + // 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution + const stateNames = { + 0: 'Pending', + 1: 'Succeeded', + 2: 'Defeated', + 3: 'Executed', + 4: 'Canceled', + 5: 'ReadyForExecution' + }; + + return { + success: true, + state: state, + stateName: stateNames[state] || 'Unknown' + }; + } catch (error) { + console.error('❌ Ошибка получения состояния предложения:', error); + return { + success: false, + error: error.message, + state: null + }; + } + }; + + /** + * Проверить результат предложения + */ + const checkProposalResult = async (contractAddress, proposalId) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + const result = await contract.value.checkProposalResult(proposalId); + + return { + success: true, + passed: result.passed, + quorumReached: result.quorumReached + }; + } catch (error) { + console.error('❌ Ошибка проверки результата предложения:', error); + return { + success: false, + error: error.message, + passed: false, + quorumReached: false + }; + } + }; + + /** + * Получить информацию о DLE + */ + const getDleInfo = async (contractAddress) => { + try { + if (!contract.value) { + initContract(contractAddress); + } + + const info = await contract.value.getDLEInfo(); + + return { + success: true, + data: { + name: info.name, + symbol: info.symbol, + location: info.location, + coordinates: info.coordinates, + jurisdiction: info.jurisdiction, + okvedCodes: info.okvedCodes, + kpp: info.kpp, + creationTimestamp: info.creationTimestamp, + isActive: info.isActive + } + }; + } catch (error) { + console.error('❌ Ошибка получения информации о DLE:', error); + return { + success: false, + error: error.message + }; + } + }; + + // Вычисляемые свойства + const isWalletConnected = computed(() => isConnected.value); + const currentUserAddress = computed(() => userAddress.value); + const currentChainId = computed(() => chainId.value); + + return { + // Состояние + isConnected, + provider, + signer, + contract, + userAddress, + chainId, + + // Вычисляемые свойства + isWalletConnected, + currentUserAddress, + currentChainId, + + // Методы + connectWallet, + initContract, + checkTokenBalance, + voteOnProposal, + executeProposal, + cancelProposal, + getProposalState, + checkProposalResult, + getDleInfo + }; +} diff --git a/frontend/src/composables/useProposalValidation.js b/frontend/src/composables/useProposalValidation.js new file mode 100644 index 0000000..a480b85 --- /dev/null +++ b/frontend/src/composables/useProposalValidation.js @@ -0,0 +1,207 @@ +/** + * Composable для валидации предложений DLE + * Проверяет реальность предложений по хешам транзакций + */ + +import { ref, computed } from 'vue'; + +export function useProposalValidation() { + const validatedProposals = ref([]); + const validationErrors = ref([]); + const isValidating = ref(false); + + // Проверка формата хеша транзакции + const isValidTransactionHash = (hash) => { + if (!hash) return false; + return /^0x[a-fA-F0-9]{64}$/.test(hash); + }; + + // Проверка формата адреса + const isValidAddress = (address) => { + if (!address) return false; + return /^0x[a-fA-F0-9]{40}$/.test(address); + }; + + // Проверка chainId + const isValidChainId = (chainId) => { + const validChainIds = [1, 11155111, 17000, 421614, 84532, 8453]; // Mainnet, Sepolia, Holesky, Arbitrum Sepolia, Base Sepolia, Base + return validChainIds.includes(Number(chainId)); + }; + + // Валидация предложения + const validateProposal = (proposal) => { + const errors = []; + + // Проверка обязательных полей + if (!proposal.id && proposal.id !== 0) { + errors.push('Отсутствует ID предложения'); + } + + if (!proposal.description || proposal.description.trim() === '') { + errors.push('Отсутствует описание предложения'); + } + + if (!proposal.transactionHash) { + errors.push('Отсутствует хеш транзакции'); + } else if (!isValidTransactionHash(proposal.transactionHash)) { + errors.push('Неверный формат хеша транзакции'); + } + + if (!proposal.initiator) { + errors.push('Отсутствует инициатор предложения'); + } else if (!isValidAddress(proposal.initiator)) { + errors.push('Неверный формат адреса инициатора'); + } + + if (!proposal.chainId) { + errors.push('Отсутствует chainId'); + } else if (!isValidChainId(proposal.chainId)) { + errors.push('Неподдерживаемый chainId'); + } + + if (proposal.state === undefined || proposal.state === null) { + errors.push('Отсутствует статус предложения'); + } + + // Проверка числовых значений + if (typeof proposal.forVotes !== 'number' || proposal.forVotes < 0) { + errors.push('Неверное значение голосов "за"'); + } + + if (typeof proposal.againstVotes !== 'number' || proposal.againstVotes < 0) { + errors.push('Неверное значение голосов "против"'); + } + + if (typeof proposal.quorumRequired !== 'number' || proposal.quorumRequired < 0) { + errors.push('Неверное значение требуемого кворума'); + } + + return { + isValid: errors.length === 0, + errors + }; + }; + + // Валидация массива предложений + const validateProposals = (proposals) => { + isValidating.value = true; + validationErrors.value = []; + validatedProposals.value = []; + + const validProposals = []; + const allErrors = []; + + proposals.forEach((proposal, index) => { + const validation = validateProposal(proposal); + + if (validation.isValid) { + validProposals.push(proposal); + } else { + allErrors.push({ + proposalIndex: index, + proposalId: proposal.id, + errors: validation.errors + }); + } + }); + + validatedProposals.value = validProposals; + validationErrors.value = allErrors; + isValidating.value = false; + + console.log(`[Proposal Validation] Проверено предложений: ${proposals.length}`); + console.log(`[Proposal Validation] Валидных: ${validProposals.length}`); + console.log(`[Proposal Validation] С ошибками: ${allErrors.length}`); + + return { + validProposals, + errors: allErrors, + totalCount: proposals.length, + validCount: validProposals.length, + errorCount: allErrors.length + }; + }; + + // Получение статистики валидации + const validationStats = computed(() => { + const total = validatedProposals.value.length + validationErrors.value.length; + const valid = validatedProposals.value.length; + const invalid = validationErrors.value.length; + + return { + total, + valid, + invalid, + validPercentage: total > 0 ? Math.round((valid / total) * 100) : 0, + invalidPercentage: total > 0 ? Math.round((invalid / total) * 100) : 0 + }; + }); + + // Проверка, является ли предложение реальным (на основе хеша транзакции) + const isRealProposal = (proposal) => { + if (!proposal.transactionHash) return false; + + // Проверяем, что хеш имеет правильный формат + if (!isValidTransactionHash(proposal.transactionHash)) return false; + + // Проверяем, что это не тестовые/фейковые хеши + const fakeHashes = [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ]; + + if (fakeHashes.includes(proposal.transactionHash.toLowerCase())) return false; + + // Проверяем, что хеш не начинается с нулей (подозрительно) + if (proposal.transactionHash.startsWith('0x0000')) return false; + + return true; + }; + + // Фильтрация только реальных предложений + const filterRealProposals = (proposals) => { + return proposals.filter(proposal => isRealProposal(proposal)); + }; + + // Фильтрация активных предложений (исключает выполненные и отмененные) + const filterActiveProposals = (proposals) => { + return proposals.filter(proposal => { + // Исключаем выполненные и отмененные предложения + if (proposal.executed || proposal.canceled) { + console.log(`🚫 [FILTER] Исключаем предложение ${proposal.id}: executed=${proposal.executed}, canceled=${proposal.canceled}`); + return false; + } + + // Исключаем предложения с истекшим deadline + if (proposal.deadline) { + const currentTime = Math.floor(Date.now() / 1000); + if (currentTime > proposal.deadline) { + console.log(`⏰ [FILTER] Исключаем предложение ${proposal.id}: deadline истек`); + return false; + } + } + + return true; + }); + }; + + return { + // Данные + validatedProposals, + validationErrors, + isValidating, + validationStats, + + // Методы + validateProposal, + validateProposals, + isRealProposal, + filterRealProposals, + filterActiveProposals, + + // Вспомогательные функции + isValidTransactionHash, + isValidAddress, + isValidChainId + }; +} diff --git a/frontend/src/composables/useProposals.js b/frontend/src/composables/useProposals.js new file mode 100644 index 0000000..4a9f4ba --- /dev/null +++ b/frontend/src/composables/useProposals.js @@ -0,0 +1,534 @@ +import { ref, computed } from 'vue'; +import { getProposals } from '@/services/proposalsService'; +import { ethers } from 'ethers'; +import { useProposalValidation } from './useProposalValidation'; +import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract'; + +// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted +// Функция checkTokenBalance перенесена в useDleContract.js + +// Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом + +export function useProposals(dleAddress, isAuthenticated, userAddress) { + const proposals = ref([]); + const filteredProposals = ref([]); + const isLoading = ref(false); + const isVoting = ref(false); + const isExecuting = ref(false); + const isCancelling = ref(false); + const statusFilter = ref(''); + const searchQuery = ref(''); + + // Используем готовые функции из utils/dle-contract.js + + // Инициализируем валидацию + const { + validateProposals, + filterRealProposals, + filterActiveProposals, + validationStats, + isValidating + } = useProposalValidation(); + + const loadProposals = async () => { + if (!dleAddress.value) { + console.warn('Адрес DLE не найден'); + return; + } + + try { + isLoading.value = true; + const response = await getProposals(dleAddress.value); + + if (response.success) { + const rawProposals = response.data.proposals || []; + + console.log(`[Proposals] Загружено предложений: ${rawProposals.length}`); + console.log(`[Proposals] Полные данные из блокчейна:`, rawProposals); + + // Детальная информация о каждом предложении + rawProposals.forEach((proposal, index) => { + console.log(`[Proposals] Предложение ${index}:`, { + id: proposal.id, + description: proposal.description, + state: proposal.state, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + quorumRequired: proposal.quorumRequired, + quorumReached: proposal.quorumReached, + executed: proposal.executed, + canceled: proposal.canceled, + initiator: proposal.initiator, + chainId: proposal.chainId, + transactionHash: proposal.transactionHash + }); + }); + + // Применяем валидацию предложений + const validationResult = validateProposals(rawProposals); + + // Фильтруем только реальные предложения + const realProposals = filterRealProposals(validationResult.validProposals); + + // Фильтруем только активные предложения (исключаем выполненные и отмененные) + const activeProposals = filterActiveProposals(realProposals); + + console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`); + console.log(`[Proposals] Реальных предложений: ${realProposals.length}`); + console.log(`[Proposals] Активных предложений: ${activeProposals.length}`); + + if (validationResult.errorCount > 0) { + console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`); + } + + proposals.value = activeProposals; + filterProposals(); + } + } catch (error) { + console.error('Ошибка загрузки предложений:', error); + } finally { + isLoading.value = false; + } + }; + + const filterProposals = () => { + if (!proposals.value || proposals.value.length === 0) { + filteredProposals.value = []; + return; + } + + let filtered = [...proposals.value]; + + if (statusFilter.value) { + filtered = filtered.filter(proposal => { + switch (statusFilter.value) { + case 'active': return proposal.state === 0; // Pending + case 'succeeded': return proposal.state === 1; // Succeeded + case 'defeated': return proposal.state === 2; // Defeated + case 'executed': return proposal.state === 3; // Executed + case 'cancelled': return proposal.state === 4; // Canceled + case 'ready': return proposal.state === 5; // ReadyForExecution + default: return true; + } + }); + } + + if (searchQuery.value) { + const query = searchQuery.value.toLowerCase(); + filtered = filtered.filter(proposal => + proposal.description.toLowerCase().includes(query) || + proposal.initiator.toLowerCase().includes(query) || + proposal.uniqueId.toLowerCase().includes(query) + ); + } + + filteredProposals.value = filtered; + }; + + const voteOnProposal = async (proposalId, support) => { + try { + console.log('🚀 [VOTE] Начинаем голосование через DLE контракт:', { proposalId, support, dleAddress: dleAddress.value, userAddress: userAddress.value }); + isVoting.value = true; + + // Проверяем наличие MetaMask + if (!window.ethereum) { + throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.'); + } + + // Проверяем состояние предложения + console.log('🔍 [DEBUG] Проверяем состояние предложения...'); + const proposal = proposals.value.find(p => p.id === proposalId); + if (!proposal) { + throw new Error('Предложение не найдено'); + } + + console.log('📊 [DEBUG] Данные предложения:', { + id: proposal.id, + state: proposal.state, + deadline: proposal.deadline, + forVotes: proposal.forVotes, + againstVotes: proposal.againstVotes, + executed: proposal.executed, + canceled: proposal.canceled + }); + + // Проверяем, что предложение активно (Pending) + if (proposal.state !== 0) { + const statusText = getProposalStatusText(proposal.state); + throw new Error(`Предложение не активно (статус: ${statusText}). Голосование возможно только для активных предложений.`); + } + + // Проверяем, что предложение не выполнено и не отменено + if (proposal.executed) { + throw new Error('Предложение уже выполнено. Голосование невозможно.'); + } + + if (proposal.canceled) { + throw new Error('Предложение отменено. Голосование невозможно.'); + } + + // Проверяем deadline + const currentTime = Math.floor(Date.now() / 1000); + if (proposal.deadline && currentTime > proposal.deadline) { + throw new Error('Время голосования истекло. Голосование невозможно.'); + } + + // Проверяем баланс токенов пользователя + console.log('💰 [DEBUG] Проверяем баланс токенов...'); + try { + const balanceCheck = await checkTokenBalance(dleAddress.value, userAddress.value); + console.log('💰 [DEBUG] Баланс токенов:', balanceCheck); + + if (!balanceCheck.hasTokens) { + throw new Error('У вас нет токенов для голосования. Необходимо иметь токены DLE для участия в голосовании.'); + } + } catch (balanceError) { + console.warn('⚠️ [DEBUG] Ошибка проверки баланса (продолжаем):', balanceError.message); + // Не останавливаем голосование, если не удалось проверить баланс + } + + // Проверяем сеть кошелька + console.log('🌐 [DEBUG] Проверяем сеть кошелька...'); + try { + const chainId = await window.ethereum.request({ method: 'eth_chainId' }); + console.log('🌐 [DEBUG] Текущая сеть:', chainId); + console.log('🌐 [DEBUG] Сеть предложения:', proposal.chainId); + + if (chainId !== proposal.chainId) { + throw new Error(`Неправильная сеть! Текущая сеть: ${chainId}, требуется: ${proposal.chainId}`); + } + } catch (networkError) { + console.warn('⚠️ [DEBUG] Ошибка проверки сети (продолжаем):', networkError.message); + } + + // Голосуем через готовую функцию из utils/dle-contract.js + console.log('🗳️ Отправляем голосование через смарт-контракт...'); + const result = await voteForProposal(dleAddress.value, proposalId, support); + + console.log('✅ Голосование успешно отправлено:', result.txHash); + alert(`Голосование успешно отправлено! Хеш транзакции: ${result.txHash}`); + + // Принудительно обновляем данные предложения + console.log('🔄 [VOTE] Обновляем данные после голосования...'); + await loadProposals(); + + // Дополнительная задержка для подтверждения в блокчейне + setTimeout(async () => { + console.log('🔄 [VOTE] Повторное обновление через 3 секунды...'); + await loadProposals(); + }, 3000); + } catch (error) { + console.error('❌ Ошибка голосования:', error); + + // Улучшенная обработка ошибок + let errorMessage = error.message; + + if (error.message.includes('execution reverted')) { + if (error.data === '0xe7005635') { + errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' + + '• Вы уже голосовали за это предложение\n' + + '• У вас нет токенов для голосования\n' + + '• Предложение не активно\n' + + '• Время голосования истекло'; + } else if (error.data === '0xc7567e07') { + errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' + + '• Вы уже голосовали за это предложение\n' + + '• У вас нет токенов для голосования\n' + + '• Предложение не активно\n' + + '• Время голосования истекло\n' + + '• Неправильная сеть для голосования'; + } else { + errorMessage = `Транзакция отклонена смарт-контрактом (код: ${error.data}). Проверьте условия голосования.`; + } + } else if (error.message.includes('user rejected')) { + errorMessage = 'Транзакция отклонена пользователем'; + } else if (error.message.includes('insufficient funds')) { + errorMessage = 'Недостаточно средств для оплаты газа'; + } + + alert('Ошибка при голосовании: ' + errorMessage); + } finally { + isVoting.value = false; + } + }; + + const executeProposal = async (proposalId) => { + try { + console.log('⚡ [EXECUTE] Исполняем предложение через DLE контракт:', { proposalId, dleAddress: dleAddress.value }); + isExecuting.value = true; + + // Проверяем состояние предложения перед выполнением + console.log('🔍 [DEBUG] Проверяем состояние предложения для выполнения...'); + const proposal = proposals.value.find(p => p.id === proposalId); + if (!proposal) { + throw new Error('Предложение не найдено'); + } + + console.log('📊 [DEBUG] Данные предложения для выполнения:', { + id: proposal.id, + state: proposal.state, + executed: proposal.executed, + canceled: proposal.canceled, + quorumReached: proposal.quorumReached + }); + + // Проверяем, что предложение можно выполнить + if (proposal.executed) { + throw new Error('Предложение уже выполнено. Повторное выполнение невозможно.'); + } + + if (proposal.canceled) { + throw new Error('Предложение отменено. Выполнение невозможно.'); + } + + // Проверяем, что предложение готово к выполнению + if (proposal.state !== 5) { + const statusText = getProposalStatusText(proposal.state); + throw new Error(`Предложение не готово к выполнению (статус: ${statusText}). Выполнение возможно только для предложений в статусе "Готово к выполнению".`); + } + + // Исполняем предложение через готовую функцию из utils/dle-contract.js + const result = await executeProposalUtil(dleAddress.value, proposalId); + + console.log('✅ Предложение успешно исполнено:', result.txHash); + alert(`Предложение успешно исполнено! Хеш транзакции: ${result.txHash}`); + + // Принудительно обновляем состояние предложения в UI + updateProposalState(proposalId, { + executed: true, + state: 1, // Выполнено + canceled: false + }); + + await loadProposals(); // Перезагружаем данные + } catch (error) { + console.error('❌ Ошибка выполнения предложения:', error); + + // Улучшенная обработка ошибок + let errorMessage = error.message; + + if (error.message.includes('execution reverted')) { + errorMessage = 'Выполнение отклонено смарт-контрактом. Возможные причины:\n' + + '• Предложение уже выполнено\n' + + '• Предложение отменено\n' + + '• Кворум не достигнут\n' + + '• Предложение не активно'; + } else if (error.message.includes('user rejected')) { + errorMessage = 'Транзакция отклонена пользователем'; + } else if (error.message.includes('insufficient funds')) { + errorMessage = 'Недостаточно средств для оплаты газа'; + } + + alert('Ошибка при исполнении предложения: ' + errorMessage); + } finally { + isExecuting.value = false; + } + }; + + const cancelProposal = async (proposalId, reason = 'Отменено пользователем') => { + try { + console.log('❌ [CANCEL] Отменяем предложение через DLE контракт:', { proposalId, reason, dleAddress: dleAddress.value }); + isCancelling.value = true; + + // Проверяем состояние предложения перед отменой + console.log('🔍 [DEBUG] Проверяем состояние предложения для отмены...'); + const proposal = proposals.value.find(p => p.id === proposalId); + if (!proposal) { + throw new Error('Предложение не найдено'); + } + + console.log('📊 [DEBUG] Данные предложения для отмены:', { + id: proposal.id, + state: proposal.state, + executed: proposal.executed, + canceled: proposal.canceled, + deadline: proposal.deadline + }); + + // Проверяем, что предложение можно отменить + if (proposal.executed) { + throw new Error('Предложение уже выполнено. Отмена невозможна.'); + } + + if (proposal.canceled) { + throw new Error('Предложение уже отменено. Повторная отмена невозможна.'); + } + + // Проверяем, что предложение активно (Pending) + if (proposal.state !== 0) { + const statusText = getProposalStatusText(proposal.state); + throw new Error(`Предложение не активно (статус: ${statusText}). Отмена возможна только для активных предложений.`); + } + + // Проверяем, что пользователь является инициатором + if (proposal.initiator !== userAddress.value) { + throw new Error('Только инициатор предложения может его отменить.'); + } + + // Проверяем deadline (нужен запас 15 минут) + const currentTime = Math.floor(Date.now() / 1000); + if (proposal.deadline) { + const timeRemaining = proposal.deadline - currentTime; + if (timeRemaining <= 900) { // 15 минут запас + throw new Error('Время для отмены истекло. Отмена возможна только за 15 минут до окончания голосования.'); + } + } + + // Отменяем предложение через готовую функцию из utils/dle-contract.js + const result = await cancelProposalUtil(dleAddress.value, proposalId, reason); + + console.log('✅ Предложение успешно отменено:', result.txHash); + alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`); + + // Принудительно обновляем состояние предложения в UI + updateProposalState(proposalId, { + canceled: true, + state: 2, // Отменено + executed: false + }); + + await loadProposals(); // Перезагружаем данные + } catch (error) { + console.error('❌ Ошибка отмены предложения:', error); + + // Улучшенная обработка ошибок + let errorMessage = error.message; + + if (error.message.includes('execution reverted')) { + errorMessage = 'Отмена отклонена смарт-контрактом. Возможные причины:\n' + + '• Предложение уже отменено\n' + + '• Предложение уже выполнено\n' + + '• Предложение не активно\n' + + '• Недостаточно прав для отмены'; + } else if (error.message.includes('user rejected')) { + errorMessage = 'Транзакция отклонена пользователем'; + } else if (error.message.includes('insufficient funds')) { + errorMessage = 'Недостаточно средств для оплаты газа'; + } + + alert('Ошибка при отмене предложения: ' + errorMessage); + } finally { + isCancelling.value = false; + } + }; + + const getProposalStatusClass = (state) => { + switch (state) { + case 0: return 'status-active'; // Pending + case 1: return 'status-succeeded'; // Succeeded + case 2: return 'status-defeated'; // Defeated + case 3: return 'status-executed'; // Executed + case 4: return 'status-cancelled'; // Canceled + case 5: return 'status-ready'; // ReadyForExecution + default: return 'status-active'; + } + }; + + const getProposalStatusText = (state) => { + switch (state) { + case 0: return 'Активное'; + case 1: return 'Успешное'; + case 2: return 'Отклоненное'; + case 3: return 'Выполнено'; + case 4: return 'Отменено'; + case 5: return 'Готово к выполнению'; + default: return 'Неизвестно'; + } + }; + + const getQuorumPercentage = (proposal) => { + // Получаем реальные данные из предложения + const forVotes = Number(proposal.forVotes || 0); + const againstVotes = Number(proposal.againstVotes || 0); + const totalVotes = forVotes + againstVotes; + + // Используем реальный totalSupply из предложения или fallback + const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE + + console.log(`📊 [QUORUM] Предложение ${proposal.id}:`, { + forVotes: forVotes, + againstVotes: againstVotes, + totalVotes: totalVotes, + totalSupply: totalSupply, + forVotesFormatted: `${(forVotes / 1e+18).toFixed(2)} DLE`, + againstVotesFormatted: `${(againstVotes / 1e+18).toFixed(2)} DLE`, + totalVotesFormatted: `${(totalVotes / 1e+18).toFixed(2)} DLE`, + totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE` + }); + + const percentage = totalSupply > 0 ? (totalVotes / totalSupply) * 100 : 0; + return percentage.toFixed(2); + }; + + const getRequiredQuorumPercentage = (proposal) => { + // Получаем требуемый кворум из предложения + const requiredQuorum = Number(proposal.quorumRequired || 0); + + // Используем реальный totalSupply из предложения или fallback + const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE + + console.log(`📊 [REQUIRED QUORUM] Предложение ${proposal.id}:`, { + requiredQuorum: requiredQuorum, + totalSupply: totalSupply, + requiredQuorumFormatted: `${(requiredQuorum / 1e+18).toFixed(2)} DLE`, + totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE` + }); + + const percentage = totalSupply > 0 ? (requiredQuorum / totalSupply) * 100 : 0; + return percentage.toFixed(2); + }; + + const canVote = (proposal) => { + return proposal.state === 0; // Pending - только активные предложения + }; + + const canExecute = (proposal) => { + return proposal.state === 5; // ReadyForExecution - готово к выполнению + }; + + const canCancel = (proposal) => { + // Можно отменить только активные предложения (Pending) + return proposal.state === 0 && + !proposal.executed && + !proposal.canceled; + }; + + // Принудительное обновление состояния предложения в UI + const updateProposalState = (proposalId, updates) => { + const proposal = proposals.value.find(p => p.id === proposalId); + if (proposal) { + Object.assign(proposal, updates); + console.log(`🔄 [UI] Обновлено состояние предложения ${proposalId}:`, updates); + + // Принудительно обновляем фильтрацию + filterProposals(); + } + }; + + return { + proposals, + filteredProposals, + isLoading, + isVoting, + isExecuting, + isCancelling, + statusFilter, + searchQuery, + loadProposals, + filterProposals, + voteOnProposal, + executeProposal, + cancelProposal, + getProposalStatusClass, + getProposalStatusText, + getQuorumPercentage, + getRequiredQuorumPercentage, + canVote, + canExecute, + canCancel, + updateProposalState, + // Валидация + validationStats, + isValidating + }; +} \ No newline at end of file diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 7f4c59c..16310e6 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -228,14 +228,9 @@ const routes = [ component: () => import('../views/smartcontracts/CreateProposalView.vue') }, { - path: '/management/tokens', - name: 'management-tokens', - component: () => import('../views/smartcontracts/TokensView.vue') - }, - { - path: '/management/quorum', - name: 'management-quorum', - component: () => import('../views/smartcontracts/QuorumView.vue') + path: '/management/add-module', + name: 'management-add-module', + component: () => import('../views/smartcontracts/AddModuleFormView.vue') }, { path: '/management/modules', diff --git a/frontend/src/services/contactsService.js b/frontend/src/services/contactsService.js index 17fe59b..370c510 100644 --- a/frontend/src/services/contactsService.js +++ b/frontend/src/services/contactsService.js @@ -57,24 +57,24 @@ export default { }, // --- Работа с тегами пользователя --- async addTagsToContact(contactId, tagIds) { - // PATCH /api/tags/user/:id { tags: [...] } + // PATCH /tags/user/:id { tags: [...] } const res = await api.patch(`/tags/user/${contactId}`, { tags: tagIds }); return res.data; }, async getContactTags(contactId) { - // GET /api/tags/user/:id + // GET /tags/user/:id const res = await api.get(`/tags/user/${contactId}`); return res.data.tags || []; }, async removeTagFromContact(contactId, tagId) { - // DELETE /api/tags/user/:id/tag/:tagId + // DELETE /tags/user/:id/tag/:tagId const res = await api.delete(`/tags/user/${contactId}/tag/${tagId}`); return res.data; } }; export async function getContacts() { - const res = await fetch('/api/users'); + const res = await fetch('/users'); const data = await res.json(); if (data && data.success) { return data.contacts; diff --git a/frontend/src/services/modulesService.js b/frontend/src/services/modulesService.js index c5623e6..055c0d7 100644 --- a/frontend/src/services/modulesService.js +++ b/frontend/src/services/modulesService.js @@ -13,6 +13,23 @@ // Сервис для работы с модулями DLE import api from '@/api/axios'; +/** + * Получить deploymentId по адресу DLE + * @param {string} dleAddress - Адрес DLE + * @returns {Promise} - Результат с deploymentId + */ +export const getDeploymentId = async (dleAddress) => { + try { + const response = await api.post('/dle-modules/get-deployment-id', { + dleAddress + }); + return response.data; + } catch (error) { + console.error('Ошибка при получении deploymentId:', error); + throw error; + } +}; + /** * Создает предложение о добавлении модуля * @param {string} dleAddress - Адрес DLE diff --git a/frontend/src/services/multichainExecutionService.js b/frontend/src/services/multichainExecutionService.js new file mode 100644 index 0000000..db9cb7d --- /dev/null +++ b/frontend/src/services/multichainExecutionService.js @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2024-2025 Тарабанов Александр Викторович + * All rights reserved. + * + * This software is proprietary and confidential. + * Unauthorized copying, modification, or distribution is prohibited. + * + * For licensing inquiries: info@hb3-accelerator.com + * Website: https://hb3-accelerator.com + * GitHub: https://github.com/HB3-ACCELERATOR + */ + +import api from '@/api/axios'; + +/** + * Получить информацию о мультиконтрактном предложении + * @param {string} dleAddress - Адрес DLE контракта + * @param {number} proposalId - ID предложения + * @param {number} governanceChainId - ID сети голосования + * @returns {Promise} - Информация о предложении + */ +export async function getProposalMultichainInfo(dleAddress, proposalId, governanceChainId) { + try { + const response = await api.post('/dle-multichain-execution/get-proposal-multichain-info', { + dleAddress, + proposalId, + governanceChainId + }); + + if (response.data.success) { + return response.data.data; + } else { + throw new Error(response.data.error || 'Не удалось получить информацию о мультиконтрактном предложении'); + } + } catch (error) { + console.error('Ошибка получения информации о мультиконтрактном предложении:', error); + throw error; + } +} + +/** + * Исполнить предложение во всех целевых сетях + * @param {string} dleAddress - Адрес DLE контракта + * @param {number} proposalId - ID предложения + * @param {string} deploymentId - ID деплоя + * @param {string} userAddress - Адрес пользователя + * @returns {Promise} - Результат исполнения + */ +export async function executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress) { + try { + const response = await api.post('/dle-multichain-execution/execute-in-all-target-chains', { + dleAddress, + proposalId, + deploymentId, + userAddress + }); + + if (response.data.success) { + return response.data.data; + } else { + throw new Error(response.data.error || 'Не удалось исполнить предложение во всех целевых сетях'); + } + } catch (error) { + console.error('Ошибка исполнения во всех целевых сетях:', error); + throw error; + } +} + +/** + * Исполнить предложение в конкретной целевой сети + * @param {string} dleAddress - Адрес DLE контракта + * @param {number} proposalId - ID предложения + * @param {number} targetChainId - ID целевой сети + * @param {string} deploymentId - ID деплоя + * @param {string} userAddress - Адрес пользователя + * @returns {Promise} - Результат исполнения + */ +export async function executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress) { + try { + const response = await api.post('/dle-multichain-execution/execute-in-target-chain', { + dleAddress, + proposalId, + targetChainId, + deploymentId, + userAddress + }); + + if (response.data.success) { + return response.data.data; + } else { + throw new Error(response.data.error || 'Не удалось исполнить предложение в целевой сети'); + } + } catch (error) { + console.error('Ошибка исполнения в целевой сети:', error); + throw error; + } +} + +/** + * Получить deploymentId по адресу DLE + * @param {string} dleAddress - Адрес DLE контракта + * @returns {Promise} - ID деплоя + */ +export async function getDeploymentId(dleAddress) { + try { + const response = await api.post('/dle-modules/get-deployment-id', { + dleAddress + }); + + if (response.data.success) { + return response.data.data.deploymentId; + } else { + throw new Error(response.data.error || 'Не удалось получить ID деплоя'); + } + } catch (error) { + console.error('Ошибка получения ID деплоя:', error); + throw error; + } +} + +/** + * Проверить, является ли предложение мультиконтрактным + * @param {Object} proposal - Предложение + * @returns {boolean} - Является ли мультиконтрактным + */ +export function isMultichainProposal(proposal) { + return proposal.targetChains && proposal.targetChains.length > 0; +} + +/** + * Получить название сети по ID + * @param {number} chainId - ID сети + * @returns {string} - Название сети + */ +export function getChainName(chainId) { + const chainNames = { + 1: 'Ethereum Mainnet', + 11155111: 'Sepolia', + 17000: 'Holesky', + 421614: 'Arbitrum Sepolia', + 84532: 'Base Sepolia', + 137: 'Polygon', + 80001: 'Polygon Mumbai', + 56: 'BSC', + 97: 'BSC Testnet' + }; + + return chainNames[chainId] || `Chain ${chainId}`; +} + +/** + * Форматировать результат исполнения + * @param {Object} result - Результат исполнения + * @returns {string} - Отформатированный результат + */ +export function formatExecutionResult(result) { + const { summary, executionResults } = result; + + if (summary.successful === summary.total) { + return `✅ Успешно исполнено во всех ${summary.total} сетях`; + } else if (summary.successful > 0) { + return `⚠️ Частично исполнено: ${summary.successful}/${summary.total} сетей`; + } else { + return `❌ Не удалось исполнить ни в одной сети`; + } +} + +/** + * Получить детали ошибок исполнения + * @param {Object} result - Результат исполнения + * @returns {Array} - Массив ошибок + */ +export function getExecutionErrors(result) { + return result.executionResults + .filter(r => !r.success) + .map(r => ({ + chainId: r.chainId, + chainName: getChainName(r.chainId), + error: r.error + })); +} + + diff --git a/frontend/src/services/proposalsService.js b/frontend/src/services/proposalsService.js index 653096b..3ddf465 100644 --- a/frontend/src/services/proposalsService.js +++ b/frontend/src/services/proposalsService.js @@ -20,7 +20,15 @@ import axios from 'axios'; */ export const getProposals = async (dleAddress) => { try { + console.log(`🌐 [API] Запрашиваем предложения для DLE: ${dleAddress}`); const response = await axios.post('/dle-proposals/get-proposals', { dleAddress }); + + console.log(`🌐 [API] Ответ от backend:`, { + success: response.data.success, + proposalsCount: response.data.data?.proposals?.length || 0, + fullResponse: response.data + }); + return response.data; } catch (error) { console.error('Ошибка при получении предложений:', error); @@ -73,13 +81,21 @@ export const createProposal = async (dleAddress, proposalData) => { * @param {boolean} support - Поддержка предложения * @returns {Promise} - Результат голосования */ -export const voteOnProposal = async (dleAddress, proposalId, support) => { +export const voteOnProposal = async (dleAddress, proposalId, support, userAddress) => { try { - const response = await axios.post('/dle-proposals/vote-proposal', { + const requestData = { dleAddress, proposalId, - support - }); + support, + voterAddress: userAddress + }; + + console.log('📤 [SERVICE] Отправляем запрос на голосование:', requestData); + + const response = await axios.post('/dle-proposals/vote-proposal', requestData); + + console.log('📥 [SERVICE] Ответ от бэкенда:', response.data); + return response.data; } catch (error) { console.error('Ошибка при голосовании:', error); diff --git a/frontend/src/services/wallet.js b/frontend/src/services/wallet.js index d7a042f..a0b2cad 100644 --- a/frontend/src/services/wallet.js +++ b/frontend/src/services/wallet.js @@ -43,6 +43,10 @@ export async function connectWithWallet() { const nonceResponse = await axios.get(`/auth/nonce?address=${address}`); const nonce = nonceResponse.data.nonce; // console.log('Got nonce:', nonce); + + if (!nonce) { + throw new Error('Не удалось получить nonce с сервера'); + } // Создаем сообщение для подписи const domain = window.location.host; @@ -73,7 +77,7 @@ export async function connectWithWallet() { // chainId: 1, // nonce, // issuedAt, - // resources: [`${origin}/api/auth/verify`], + // resources: [`${origin}/auth/verify`], // }); // Запрашиваем подпись diff --git a/frontend/src/utils/dle-abi.js b/frontend/src/utils/dle-abi.js new file mode 100644 index 0000000..ae3a971 --- /dev/null +++ b/frontend/src/utils/dle-abi.js @@ -0,0 +1,106 @@ +/** + * ABI для DLE смарт-контракта + * АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ + * Для обновления запустите: node backend/scripts/generate-abi.js + * + * Последнее обновление: 2025-09-29T18:16:32.027Z + */ + +export const DLE_ABI = [ + "function CLOCK_MODE() returns (string)", + "function DOMAIN_SEPARATOR() returns (bytes32)", + "function activeModules(bytes32 ) returns (bool)", + "function allProposalIds(uint256 ) returns (uint256)", + "function allowance(address owner, address spender) returns (uint256)", + "function approve(address , uint256 ) returns (bool)", + "function balanceOf(address account) returns (uint256)", + "function cancelProposal(uint256 _proposalId, string reason)", + "function checkProposalResult(uint256 _proposalId) returns (bool, bool)", + "function checkpoints(address account, uint32 pos) returns (tuple)", + "function clock() returns (uint48)", + "function createAddModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) returns (uint256)", + "function createProposal(string _description, uint256 _duration, bytes _operation, uint256 _governanceChainId, uint256[] _targetChains, uint256 ) returns (uint256)", + "function createRemoveModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) returns (uint256)", + "function decimals() returns (uint8)", + "function delegate(address delegatee)", + "function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)", + "function delegates(address account) returns (address)", + "function dleInfo() returns (string, string, string, string, uint256, uint256, uint256, bool)", + "function eip712Domain() returns (bytes1, string, string, uint256, address, bytes32, uint256[])", + "function executeProposal(uint256 _proposalId)", + "function executeProposalBySignatures(uint256 _proposalId, address[] signers, bytes[] signatures)", + "function getCurrentChainId() returns (uint256)", + "function getDLEInfo() returns (tuple)", + "function getModuleAddress(bytes32 _moduleId) returns (address)", + "function getMultichainAddresses() returns (uint256[], address[])", + "function getMultichainInfo() returns (uint256[], uint256)", + "function getMultichainMetadata() returns (string)", + "function getPastTotalSupply(uint256 timepoint) returns (uint256)", + "function getPastVotes(address account, uint256 timepoint) returns (uint256)", + "function getProposalState(uint256 _proposalId) returns (uint8)", + "function getProposalSummary(uint256 _proposalId) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, uint256, uint256, uint256[])", + "function getSupportedChainCount() returns (uint256)", + "function getSupportedChainId(uint256 _index) returns (uint256)", + "function getVotes(address account) returns (uint256)", + "function initializeLogoURI(string _logoURI)", + "function initializer() returns (address)", + "function isActive() returns (bool)", + "function isChainSupported(uint256 _chainId) returns (bool)", + "function isModuleActive(bytes32 _moduleId) returns (bool)", + "function logo() returns (string)", + "function logoURI() returns (string)", + "function maxVotingDuration() returns (uint256)", + "function minVotingDuration() returns (uint256)", + "function modules(bytes32 ) returns (address)", + "function name() returns (string)", + "function nonces(address owner) returns (uint256)", + "function numCheckpoints(address account) returns (uint32)", + "function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)", + "function proposalCounter() returns (uint256)", + "function proposals(uint256 ) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, bytes, uint256, uint256)", + "function quorumPercentage() returns (uint256)", + "function supportedChainIds(uint256 ) returns (uint256)", + "function supportedChains(uint256 ) returns (bool)", + "function symbol() returns (string)", + "function tokenURI() returns (string)", + "function totalSupply() returns (uint256)", + "function transfer(address , uint256 ) returns (bool)", + "function transferFrom(address , address , uint256 ) returns (bool)", + "function vote(uint256 _proposalId, bool _support)", + "event Approval(address owner, address spender, uint256 value)", + "event ChainAdded(uint256 chainId)", + "event ChainRemoved(uint256 chainId)", + "event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp)", + "event DLEInitialized(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, address tokenAddress, uint256[] supportedChainIds)", + "event DelegateChanged(address delegator, address fromDelegate, address toDelegate)", + "event DelegateVotesChanged(address delegate, uint256 previousVotes, uint256 newVotes)", + "event EIP712DomainChanged()", + "event InitialTokensDistributed(address[] partners, uint256[] amounts)", + "event LogoURIUpdated(string oldURI, string newURI)", + "event ModuleAdded(bytes32 moduleId, address moduleAddress)", + "event ModuleRemoved(bytes32 moduleId)", + "event ProposalCancelled(uint256 proposalId, string reason)", + "event ProposalCreated(uint256 proposalId, address initiator, string description)", + "event ProposalExecuted(uint256 proposalId, bytes operation)", + "event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId)", + "event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId)", + "event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains)", + "event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower)", + "event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage)", + "event TokensTransferredByGovernance(address recipient, uint256 amount)", + "event Transfer(address from, address to, uint256 value)", + "event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration)", +]; + + +// ABI для деактивации (специальные функции) - НЕ СУЩЕСТВУЮТ В КОНТРАКТЕ +export const DLE_DEACTIVATION_ABI = [ + // Эти функции не существуют в контракте DLE +]; + +// ABI для токенов (базовые функции) +export const TOKEN_ABI = [ + "function balanceOf(address owner) view returns (uint256)", + "function decimals() view returns (uint8)", + "function totalSupply() view returns (uint256)" +]; diff --git a/frontend/src/utils/dle-contract.js b/frontend/src/utils/dle-contract.js index 76f22e7..1dc90b3 100644 --- a/frontend/src/utils/dle-contract.js +++ b/frontend/src/utils/dle-contract.js @@ -12,6 +12,91 @@ import api from '@/api/axios'; import { ethers } from 'ethers'; +import { DLE_ABI, DLE_DEACTIVATION_ABI, TOKEN_ABI } from './dle-abi'; + +// Функция для переключения сети кошелька +export async function switchToVotingNetwork(chainId) { + try { + console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId}...`); + + // Конфигурации сетей + const networks = { + '11155111': { // Sepolia + chainId: '0xaa36a7', + chainName: 'Sepolia', + nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://1rpc.io/sepolia'], + blockExplorerUrls: ['https://sepolia.etherscan.io'] + }, + '17000': { // Holesky + chainId: '0x4268', + chainName: 'Holesky', + nativeCurrency: { name: 'Holesky Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://ethereum-holesky.publicnode.com'], + blockExplorerUrls: ['https://holesky.etherscan.io'] + }, + '421614': { // Arbitrum Sepolia + chainId: '0x66eee', + chainName: 'Arbitrum Sepolia', + nativeCurrency: { name: 'Arbitrum Sepolia Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'], + blockExplorerUrls: ['https://sepolia.arbiscan.io'] + }, + '84532': { // Base Sepolia + chainId: '0x14a34', + chainName: 'Base Sepolia', + nativeCurrency: { name: 'Base Sepolia Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: ['https://sepolia.base.org'], + blockExplorerUrls: ['https://sepolia.basescan.org'] + } + }; + + const networkConfig = networks[chainId]; + if (!networkConfig) { + console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId}`); + return false; + } + + // Проверяем, подключена ли уже нужная сеть + const currentChainId = await window.ethereum.request({ method: 'eth_chainId' }); + if (currentChainId === networkConfig.chainId) { + console.log(`✅ [NETWORK] Сеть ${chainId} уже подключена`); + return true; + } + + // Пытаемся переключиться на нужную сеть + try { + await window.ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: networkConfig.chainId }] + }); + console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainId}`); + return true; + } catch (switchError) { + // Если сеть не добавлена, добавляем её + if (switchError.code === 4902) { + console.log(`➕ [NETWORK] Добавляем сеть ${chainId}...`); + try { + await window.ethereum.request({ + method: 'wallet_addEthereumChain', + params: [networkConfig] + }); + console.log(`✅ [NETWORK] Сеть ${chainId} добавлена и подключена`); + return true; + } catch (addError) { + console.error(`❌ [NETWORK] Ошибка добавления сети ${chainId}:`, addError); + return false; + } + } else { + console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainId}:`, switchError); + return false; + } + } + } catch (error) { + console.error(`❌ [NETWORK] Общая ошибка переключения сети:`, error); + return false; + } +} /** * Проверить подключение к браузерному кошельку @@ -60,6 +145,8 @@ export async function checkWalletConnection() { * Используется только система голосования (proposals) */ + + /** * Получить информацию о DLE из блокчейна * @param {string} dleAddress - Адрес DLE контракта @@ -109,12 +196,9 @@ export async function createProposal(dleAddress, proposalData) { const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - // ABI для создания предложения - const dleAbi = [ - "function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)" - ]; + // Используем общий ABI - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_ABI, signer); // Создаем предложение const tx = await dle.createProposal( @@ -162,14 +246,111 @@ export async function voteForProposal(dleAddress, proposalId, support) { const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - // ABI для голосования - const dleAbi = [ - "function vote(uint256 _proposalId, bool _support) external" - ]; + // Используем общий ABI + let dle = new ethers.Contract(dleAddress, DLE_ABI, signer); - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + // Дополнительная диагностика перед голосованием + try { + console.log('🔍 [VOTE DEBUG] Проверяем состояние предложения...'); + const proposalState = await dle.getProposalState(proposalId); + console.log('🔍 [VOTE DEBUG] Состояние предложения:', proposalState); + + // Проверяем, можно ли голосовать (состояние должно быть 0 = Pending) + if (Number(proposalState) !== 0) { + throw new Error(`Предложение в состоянии ${proposalState}, голосование невозможно`); + } + + console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования'); + + // Проверяем сеть голосования + try { + const proposal = await dle.proposals(proposalId); + const currentChainId = await dle.getCurrentChainId(); + const governanceChainId = proposal.governanceChainId; + + console.log('🔍 [VOTE DEBUG] Текущая сеть контракта:', currentChainId.toString()); + console.log('🔍 [VOTE DEBUG] Сеть голосования предложения:', governanceChainId.toString()); + + if (currentChainId.toString() !== governanceChainId.toString()) { + console.log('🔄 [VOTE DEBUG] Неправильная сеть! Пытаемся переключиться...'); + + // Пытаемся переключить сеть + const switched = await switchToVotingNetwork(governanceChainId.toString()); + if (switched) { + console.log('✅ [VOTE DEBUG] Сеть успешно переключена, переподключаемся к контракту...'); + + // Определяем правильный адрес контракта для сети голосования + let correctContractAddress = dleAddress; + + // Если контракт развернут в другой сети, нужно найти контракт в нужной сети + if (currentChainId.toString() !== governanceChainId.toString()) { + console.log('🔍 [VOTE DEBUG] Ищем контракт в сети голосования...'); + + try { + // Получаем информацию о мультичейн развертывании из БД + const response = await fetch('/api/dle-core/get-multichain-contracts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + originalContract: dleAddress, + targetChainId: governanceChainId.toString() + }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.contractAddress) { + correctContractAddress = data.contractAddress; + console.log('🔍 [VOTE DEBUG] Найден контракт в сети голосования:', correctContractAddress); + } else { + console.warn('⚠️ [VOTE DEBUG] Контракт в сети голосования не найден, используем исходный'); + } + } else { + console.warn('⚠️ [VOTE DEBUG] Ошибка получения контракта из БД, используем исходный'); + } + } catch (error) { + console.warn('⚠️ [VOTE DEBUG] Ошибка поиска контракта, используем исходный:', error.message); + } + } + + // Переподключаемся к контракту в новой сети + const newProvider = new ethers.BrowserProvider(window.ethereum); + const newSigner = await newProvider.getSigner(); + dle = new ethers.Contract(correctContractAddress, DLE_ABI, newSigner); + + // Проверяем, что теперь все корректно + const newCurrentChainId = await dle.getCurrentChainId(); + console.log('🔍 [VOTE DEBUG] Новая текущая сеть контракта:', newCurrentChainId.toString()); + + if (newCurrentChainId.toString() === governanceChainId.toString()) { + console.log('✅ [VOTE DEBUG] Сеть для голосования теперь корректна'); + } else { + throw new Error(`Не удалось переключиться на правильную сеть. Текущая: ${newCurrentChainId}, требуется: ${governanceChainId}`); + } + } else { + throw new Error(`Неправильная сеть! Контракт в сети ${currentChainId}, а голосование должно быть в сети ${governanceChainId}. Переключите кошелек вручную.`); + } + } else { + console.log('🔍 [VOTE DEBUG] Сеть для голосования корректна'); + } + + // Проверяем право голоса + const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint); + console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString()); + if (votingPower === 0n) { + throw new Error('У пользователя нет права голоса (votingPower = 0)'); + } + console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса'); + } catch (votingPowerError) { + console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message); + } + + } catch (debugError) { + console.warn('⚠️ [VOTE DEBUG] Ошибка диагностики (продолжаем):', debugError.message); + } // Голосуем за предложение + console.log('🗳️ [VOTE] Отправляем транзакцию голосования...'); const tx = await dle.vote(proposalId, support); // Ждем подтверждения транзакции @@ -182,10 +363,40 @@ export async function voteForProposal(dleAddress, proposalId, support) { blockNumber: receipt.blockNumber }; - } catch (error) { - console.error('Ошибка голосования:', error); - throw error; - } + } catch (error) { + console.error('Ошибка голосования:', error); + + // Детальная диагностика ошибки + if (error.code === 'CALL_EXCEPTION' && error.data) { + console.error('🔍 [ERROR DEBUG] Детали ошибки:', { + code: error.code, + data: error.data, + reason: error.reason, + action: error.action + }); + + // Расшифровка кода ошибки + if (error.data === '0x2eaf0f6d') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrWrongChain - неправильная сеть для голосования'); + } else if (error.data === '0xe7005635') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrAlreadyVoted - пользователь уже голосовал по этому предложению'); + } else if (error.data === '0x21c19873') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrNoPower - у пользователя нет права голоса'); + } else if (error.data === '0x834d7b85') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalMissing - предложение не найдено'); + } else if (error.data === '0xd6792fad') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalEnded - время голосования истекло'); + } else if (error.data === '0x2d686f73') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalExecuted - предложение уже исполнено'); + } else if (error.data === '0xc7567e07') { + console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalCanceled - предложение отменено'); + } else { + console.error('❌ [ERROR DEBUG] Неизвестная ошибка:', error.data); + } + } + + throw error; + } } /** @@ -206,12 +417,9 @@ export async function executeProposal(dleAddress, proposalId) { const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - // ABI для исполнения предложения - const dleAbi = [ - "function executeProposal(uint256 _proposalId) external" - ]; + // Используем общий ABI - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_ABI, signer); // Исполняем предложение const tx = await dle.executeProposal(proposalId); @@ -233,30 +441,112 @@ export async function executeProposal(dleAddress, proposalId) { } /** - * Создать предложение о добавлении модуля + * Отменить предложение + * @param {string} dleAddress - Адрес DLE контракта + * @param {number} proposalId - ID предложения + * @param {string} reason - Причина отмены + * @returns {Promise} - Результат отмены + */ +export async function cancelProposal(dleAddress, proposalId, reason) { + try { + // Проверяем наличие браузерного кошелька + if (!window.ethereum) { + throw new Error('Браузерный кошелек не установлен'); + } + + // Запрашиваем подключение к кошельку + const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); + const provider = new ethers.BrowserProvider(window.ethereum); + const signer = await provider.getSigner(); + + // Используем общий ABI + const dle = new ethers.Contract(dleAddress, DLE_ABI, signer); + + // Отменяем предложение + const tx = await dle.cancelProposal(proposalId, reason); + + // Ждем подтверждения транзакции + const receipt = await tx.wait(); + + console.log('Предложение отменено, tx hash:', tx.hash); + + return { + txHash: tx.hash, + blockNumber: receipt.blockNumber + }; + } catch (error) { + console.error('Ошибка отмены предложения:', error); + throw error; + } +} + +/** + * Проверить баланс токенов пользователя + * @param {string} dleAddress - Адрес DLE контракта + * @param {string} userAddress - Адрес пользователя + * @returns {Promise} - Баланс токенов + */ +export async function checkTokenBalance(dleAddress, userAddress) { + try { + // Проверяем наличие браузерного кошелька + if (!window.ethereum) { + throw new Error('Браузерный кошелек не установлен'); + } + + // Создаем провайдер (только для чтения) + const provider = new ethers.BrowserProvider(window.ethereum); + const dle = new ethers.Contract(dleAddress, DLE_ABI, provider); + + // Получаем баланс токенов + const balance = await dle.balanceOf(userAddress); + const balanceFormatted = ethers.formatEther(balance); + + console.log(`💰 Баланс токенов для ${userAddress}: ${balanceFormatted}`); + + return { + balance: balanceFormatted, + hasTokens: balance > 0, + rawBalance: balance.toString() + }; + } catch (error) { + console.error('Ошибка проверки баланса токенов:', error); + throw error; + } +} + +/** + * Создать предложение о добавлении модуля (с автоматической оплатой газа) * @param {string} dleAddress - Адрес DLE контракта * @param {string} description - Описание предложения * @param {number} duration - Длительность голосования в секундах * @param {string} moduleId - ID модуля * @param {string} moduleAddress - Адрес модуля * @param {number} chainId - ID цепочки для голосования + * @param {string} deploymentId - ID деплоя для получения приватного ключа (опционально) * @returns {Promise} - Результат создания предложения */ -export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) { +export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId, deploymentId = null) { try { - const response = await api.post('/blockchain/create-add-module-proposal', { + const requestData = { dleAddress: dleAddress, description: description, duration: duration, moduleId: moduleId, moduleAddress: moduleAddress, chainId: chainId - }); + }; + + // Добавляем deploymentId если он передан + if (deploymentId) { + requestData.deploymentId = deploymentId; + } + + const response = await api.post('/dle-modules/create-add-module-proposal', requestData); if (response.data.success) { return response.data.data; } else { - throw new Error(response.data.message || 'Не удалось создать предложение о добавлении модуля'); + throw new Error(response.data.error || 'Не удалось создать предложение о добавлении модуля'); } } catch (error) { console.error('Ошибка создания предложения о добавлении модуля:', error); @@ -537,6 +827,7 @@ export async function getSupportedChains(dleAddress) { * @param {string} userAddress - Адрес пользователя * @returns {Promise} - Результат деактивации */ +// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ export async function deactivateDLE(dleAddress, userAddress) { try { // Проверяем наличие браузерного кошелька @@ -568,15 +859,9 @@ export async function deactivateDLE(dleAddress, userAddress) { console.log('Проверка деактивации прошла успешно, выполняем деактивацию...'); - // ABI для деактивации DLE - const dleAbi = [ - "function deactivate() external", - "function balanceOf(address) external view returns (uint256)", - "function totalSupply() external view returns (uint256)", - "function isActive() external view returns (bool)" - ]; + // Используем общий ABI для деактивации - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_ABI, signer); // Дополнительные проверки перед деактивацией const balance = await dle.balanceOf(userAddress); @@ -640,6 +925,7 @@ export async function deactivateDLE(dleAddress, userAddress) { * @param {number} chainId - ID цепочки для деактивации * @returns {Promise} - Результат создания предложения */ +// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ export async function createDeactivationProposal(dleAddress, description, duration, chainId) { try { // Проверяем наличие браузерного кошелька @@ -650,11 +936,9 @@ export async function createDeactivationProposal(dleAddress, description, durati const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - const dleAbi = [ - "function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)" - ]; + // Используем общий ABI для деактивации - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer); const tx = await dle.createDeactivationProposal(description, duration, chainId); const receipt = await tx.wait(); @@ -681,6 +965,7 @@ export async function createDeactivationProposal(dleAddress, description, durati * @param {boolean} support - Поддержка предложения * @returns {Promise} - Результат голосования */ +// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ export async function voteDeactivationProposal(dleAddress, proposalId, support) { try { if (!window.ethereum) { @@ -690,11 +975,9 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support) const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - const dleAbi = [ - "function voteDeactivation(uint256 _proposalId, bool _support) external" - ]; + // Используем общий ABI для деактивации - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer); const tx = await dle.voteDeactivation(proposalId, support); const receipt = await tx.wait(); @@ -744,6 +1027,7 @@ export async function checkDeactivationProposalResult(dleAddress, proposalId) { * @param {number} proposalId - ID предложения * @returns {Promise} - Результат исполнения */ +// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ export async function executeDeactivationProposal(dleAddress, proposalId) { try { if (!window.ethereum) { @@ -753,11 +1037,9 @@ export async function executeDeactivationProposal(dleAddress, proposalId) { const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - const dleAbi = [ - "function executeDeactivationProposal(uint256 _proposalId) external" - ]; + // Используем общий ABI для деактивации - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer); const tx = await dle.executeDeactivationProposal(proposalId); const receipt = await tx.wait(); @@ -823,12 +1105,9 @@ export async function createTransferTokensProposal(dleAddress, transferData) { const provider = new ethers.BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - // ABI для создания предложения - const dleAbi = [ - "function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)" - ]; + // Используем общий ABI - const dle = new ethers.Contract(dleAddress, dleAbi, signer); + const dle = new ethers.Contract(dleAddress, DLE_ABI, signer); // Кодируем операцию перевода токенов const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)"); @@ -872,4 +1151,75 @@ export async function createTransferTokensProposal(dleAddress, transferData) { console.error('Ошибка создания предложения о переводе токенов:', error); throw error; } +} + +/** + * Исполнить мультиконтрактное предложение во всех целевых сетях + * @param {string} dleAddress - Адрес DLE контракта + * @param {number} proposalId - ID предложения + * @param {string} userAddress - Адрес пользователя + * @returns {Promise} - Результат исполнения + */ +export async function executeMultichainProposal(dleAddress, proposalId, userAddress) { + try { + // Импортируем сервис мультиконтрактного исполнения + const { + executeInAllTargetChains, + getDeploymentId, + formatExecutionResult, + getExecutionErrors + } = await import('@/services/multichainExecutionService'); + + // Получаем ID деплоя + const deploymentId = await getDeploymentId(dleAddress); + + // Исполняем во всех целевых сетях + const result = await executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress); + + return { + success: true, + result, + summary: formatExecutionResult(result), + errors: getExecutionErrors(result) + }; + + } catch (error) { + console.error('Ошибка исполнения мультиконтрактного предложения:', error); + throw error; + } +} + +/** + * Исполнить мультиконтрактное предложение в конкретной сети + * @param {string} dleAddress - Адрес DLE контракта + * @param {number} proposalId - ID предложения + * @param {number} targetChainId - ID целевой сети + * @param {string} userAddress - Адрес пользователя + * @returns {Promise} - Результат исполнения + */ +export async function executeMultichainProposalInChain(dleAddress, proposalId, targetChainId, userAddress) { + try { + // Импортируем сервис мультиконтрактного исполнения + const { + executeInTargetChain, + getDeploymentId, + getChainName + } = await import('@/services/multichainExecutionService'); + + // Получаем ID деплоя + const deploymentId = await getDeploymentId(dleAddress); + + // Исполняем в конкретной сети + const result = await executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress); + + return { + success: true, + result, + chainName: getChainName(targetChainId) + }; + + } catch (error) { + console.error('Ошибка исполнения мультиконтрактного предложения в сети:', error); + throw error; + } } \ No newline at end of file diff --git a/frontend/src/utils/networkConfig.js b/frontend/src/utils/networkConfig.js new file mode 100644 index 0000000..414bd29 --- /dev/null +++ b/frontend/src/utils/networkConfig.js @@ -0,0 +1,105 @@ +/** + * Конфигурации сетей блокчейна для DLE + * + * Author: HB3 Accelerator + * For licensing inquiries: info@hb3-accelerator.com + * Website: https://hb3-accelerator.com + * GitHub: https://github.com/HB3-ACCELERATOR + */ + +export const SUPPORTED_NETWORKS = { + 1: { + chainId: '0x1', + chainName: 'Ethereum Mainnet', + nativeCurrency: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://mainnet.infura.io/v3/'], + blockExplorerUrls: ['https://etherscan.io'], + }, + 11155111: { + chainId: '0xaa36a7', + chainName: 'Sepolia', + nativeCurrency: { + name: 'Sepolia Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://sepolia.infura.io/v3/', 'https://1rpc.io/sepolia'], + blockExplorerUrls: ['https://sepolia.etherscan.io'], + }, + 17000: { + chainId: '0x4268', + chainName: 'Holesky', + nativeCurrency: { + name: 'Holesky Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://ethereum-holesky.publicnode.com'], + blockExplorerUrls: ['https://holesky.etherscan.io'], + }, + 421614: { + chainId: '0x66eee', + chainName: 'Arbitrum Sepolia', + nativeCurrency: { + name: 'Arbitrum Sepolia Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'], + blockExplorerUrls: ['https://sepolia.arbiscan.io'], + }, + 84532: { + chainId: '0x14a34', + chainName: 'Base Sepolia', + nativeCurrency: { + name: 'Base Sepolia Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://sepolia.base.org'], + blockExplorerUrls: ['https://sepolia.basescan.org'], + }, + 8453: { + chainId: '0x2105', + chainName: 'Base', + nativeCurrency: { + name: 'Base Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://mainnet.base.org'], + blockExplorerUrls: ['https://basescan.org'], + }, +}; + +/** + * Получить конфигурацию сети по chainId + * @param {number|string} chainId - ID сети + * @returns {Object|null} - Конфигурация сети или null + */ +export function getNetworkConfig(chainId) { + const numericChainId = typeof chainId === 'string' ? parseInt(chainId, 16) : chainId; + return SUPPORTED_NETWORKS[numericChainId] || null; +} + +/** + * Получить hex представление chainId + * @param {number} chainId - ID сети + * @returns {string} - Hex представление + */ +export function getHexChainId(chainId) { + return `0x${chainId.toString(16)}`; +} + +/** + * Проверить, поддерживается ли сеть + * @param {number|string} chainId - ID сети + * @returns {boolean} - Поддерживается ли сеть + */ +export function isNetworkSupported(chainId) { + return getNetworkConfig(chainId) !== null; +} diff --git a/frontend/src/utils/networkSwitcher.js b/frontend/src/utils/networkSwitcher.js new file mode 100644 index 0000000..78f12d1 --- /dev/null +++ b/frontend/src/utils/networkSwitcher.js @@ -0,0 +1,157 @@ +/** + * Утилиты для переключения сетей блокчейна + * + * Author: HB3 Accelerator + * For licensing inquiries: info@hb3-accelerator.com + * Website: https://hb3-accelerator.com + * GitHub: https://github.com/HB3-ACCELERATOR + */ + +import { getNetworkConfig, getHexChainId, isNetworkSupported } from './networkConfig.js'; + +/** + * Переключить сеть в MetaMask + * @param {number} targetChainId - ID целевой сети + * @returns {Promise} - Результат переключения + */ +export async function switchNetwork(targetChainId) { + try { + console.log(`🔄 [Network Switch] Переключаемся на сеть ${targetChainId}...`); + + // Проверяем, поддерживается ли сеть + if (!isNetworkSupported(targetChainId)) { + throw new Error(`Сеть ${targetChainId} не поддерживается`); + } + + // Проверяем наличие MetaMask + if (!window.ethereum) { + throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.'); + } + + // Получаем конфигурацию сети + const networkConfig = getNetworkConfig(targetChainId); + if (!networkConfig) { + throw new Error(`Конфигурация для сети ${targetChainId} не найдена`); + } + + // Проверяем текущую сеть + const currentChainId = await window.ethereum.request({ method: 'eth_chainId' }); + console.log(`🔄 [Network Switch] Текущая сеть: ${currentChainId}, Целевая: ${getHexChainId(targetChainId)}`); + + // Если уже в нужной сети, возвращаем успех + if (currentChainId === getHexChainId(targetChainId)) { + console.log(`✅ [Network Switch] Уже в сети ${targetChainId}`); + return { + success: true, + message: `Уже в сети ${networkConfig.chainName}`, + chainId: targetChainId, + chainName: networkConfig.chainName + }; + } + + // Пытаемся переключиться на существующую сеть + try { + await window.ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: getHexChainId(targetChainId) }], + }); + + console.log(`✅ [Network Switch] Успешно переключились на ${networkConfig.chainName}`); + return { + success: true, + message: `Переключились на ${networkConfig.chainName}`, + chainId: targetChainId, + chainName: networkConfig.chainName + }; + + } catch (switchError) { + // Если сеть не добавлена в MetaMask, добавляем её + if (switchError.code === 4902) { + console.log(`➕ [Network Switch] Добавляем сеть ${networkConfig.chainName} в MetaMask...`); + + try { + await window.ethereum.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId: getHexChainId(targetChainId), + chainName: networkConfig.chainName, + nativeCurrency: networkConfig.nativeCurrency, + rpcUrls: networkConfig.rpcUrls, + blockExplorerUrls: networkConfig.blockExplorerUrls, + }], + }); + + console.log(`✅ [Network Switch] Сеть ${networkConfig.chainName} добавлена и активирована`); + return { + success: true, + message: `Сеть ${networkConfig.chainName} добавлена и активирована`, + chainId: targetChainId, + chainName: networkConfig.chainName + }; + + } catch (addError) { + console.error(`❌ [Network Switch] Ошибка добавления сети:`, addError); + throw new Error(`Не удалось добавить сеть ${networkConfig.chainName}: ${addError.message}`); + } + } else { + // Другие ошибки переключения + console.error(`❌ [Network Switch] Ошибка переключения сети:`, switchError); + throw new Error(`Не удалось переключиться на ${networkConfig.chainName}: ${switchError.message}`); + } + } + + } catch (error) { + console.error(`❌ [Network Switch] Ошибка:`, error); + return { + success: false, + error: error.message, + chainId: targetChainId + }; + } +} + +/** + * Проверить текущую сеть + * @returns {Promise} - Информация о текущей сети + */ +export async function getCurrentNetwork() { + try { + if (!window.ethereum) { + throw new Error('MetaMask не найден'); + } + + const chainId = await window.ethereum.request({ method: 'eth_chainId' }); + const numericChainId = parseInt(chainId, 16); + const networkConfig = getNetworkConfig(numericChainId); + + return { + success: true, + chainId: numericChainId, + hexChainId: chainId, + chainName: networkConfig?.chainName || 'Неизвестная сеть', + isSupported: isNetworkSupported(numericChainId) + }; + + } catch (error) { + console.error('❌ [Network Check] Ошибка:', error); + return { + success: false, + error: error.message + }; + } +} + +/** + * Получить список поддерживаемых сетей + * @returns {Array} - Список поддерживаемых сетей + */ +export function getSupportedNetworks() { + return Object.entries(SUPPORTED_NETWORKS).map(([chainId, config]) => ({ + chainId: parseInt(chainId), + hexChainId: getHexChainId(parseInt(chainId)), + chainName: config.chainName, + nativeCurrency: config.nativeCurrency, + rpcUrls: config.rpcUrls, + blockExplorerUrls: config.blockExplorerUrls + })); +} diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 217a18a..1e35e4e 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -37,6 +37,9 @@ class WebSocketClient { console.log('[WebSocket] Подключение установлено'); this.isConnected = true; this.reconnectAttempts = 0; + + // Уведомляем о подключении + this.emit('connected'); }; this.ws.onmessage = (event) => { @@ -120,6 +123,15 @@ class WebSocketClient { } } + // Эмиссия события + emit(event, data) { + if (this.listeners.has(event)) { + this.listeners.get(event).forEach(callback => { + callback(data); + }); + } + } + // Алиас для on() - для совместимости с useDeploymentWebSocket subscribe(event, callback) { this.on(event, callback); diff --git a/frontend/src/views/CrmView.vue b/frontend/src/views/CrmView.vue index 925144b..3619a93 100644 --- a/frontend/src/views/CrmView.vue +++ b/frontend/src/views/CrmView.vue @@ -156,12 +156,8 @@ let unsubscribe = null; onMounted(() => { // console.log('[CrmView] Компонент загружен'); - // Если пользователь авторизован, загружаем данные - if (auth.isAuthenticated.value) { - loadDLEs(); - } else { - isLoading.value = false; - } + // Загружаем DLE для всех пользователей (авторизованных и неавторизованных) + loadDLEs(); // Подписка на события авторизации unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent); diff --git a/frontend/src/views/ManagementView.vue b/frontend/src/views/ManagementView.vue index 9f107dc..81bd593 100644 --- a/frontend/src/views/ManagementView.vue +++ b/frontend/src/views/ManagementView.vue @@ -40,6 +40,7 @@ +

Загрузка деплоированных DLE...

@@ -56,6 +57,7 @@ class="dle-card" @click="openDleManagement(dle.dleAddress)" > +
Адреса контрактов:
- {{ getChainName(network.chainId) }}: + {{ getChainName(chainId) }}: - {{ shortenAddress(network.address) }} + {{ shortenAddress(dle.dleAddress) }}
@@ -253,14 +255,22 @@ async function loadDeployedDles() { const dlesWithBlockchainData = await Promise.all( dlesFromApi.map(async (dle) => { try { - console.log(`[ManagementView] Читаем данные из блокчейна для ${dle.dleAddress}`); + // Используем адрес из deployedNetworks если dleAddress null + const dleAddress = dle.dleAddress || (dle.deployedNetworks && dle.deployedNetworks.length > 0 ? dle.deployedNetworks[0].address : null); + + if (!dleAddress) { + console.warn(`[ManagementView] Нет адреса для DLE ${dle.deployment_id || 'unknown'}`); + return dle; + } + + console.log(`[ManagementView] Читаем данные из блокчейна для ${dleAddress}`); // Читаем данные из блокчейна const blockchainResponse = await api.post('/blockchain/read-dle-info', { - dleAddress: dle.dleAddress + dleAddress: dleAddress }); - console.log(`[ManagementView] Ответ от блокчейна для ${dle.dleAddress}:`, blockchainResponse.data); + console.log(`[ManagementView] Ответ от блокчейна для ${dleAddress}:`, blockchainResponse.data); if (blockchainResponse.data.success) { const blockchainData = blockchainResponse.data.data; @@ -376,7 +386,15 @@ function formatTokenAmount(amount) { 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 }); } @@ -812,6 +830,7 @@ onMounted(() => { } + /* Адаптивность */ @media (max-width: 768px) { .dle-title-section { diff --git a/frontend/src/views/settings/AuthTokensSettings.vue b/frontend/src/views/settings/AuthTokensSettings.vue index a1dd4d4..b1e7fee 100644 --- a/frontend/src/views/settings/AuthTokensSettings.vue +++ b/frontend/src/views/settings/AuthTokensSettings.vue @@ -84,8 +84,11 @@ v-model.number="newToken.minBalance" class="form-control" placeholder="0" + min="0" + step="0.01" :disabled="!canEdit" > + Минимальный баланс токена для получения доступа
@@ -158,6 +161,12 @@ async function addToken() { return; } + // Валидация порогов доступа + if (newToken.readonlyThreshold >= newToken.editorThreshold) { + alert('Минимум токенов для Read-Only доступа должен быть меньше минимума для Editor доступа'); + return; + } + const tokenData = { name: newToken.name, address: newToken.address, diff --git a/frontend/src/views/settings/Interface/InterfaceWebSshView.vue b/frontend/src/views/settings/Interface/InterfaceWebSshView.vue index 5968367..cc848f7 100644 --- a/frontend/src/views/settings/Interface/InterfaceWebSshView.vue +++ b/frontend/src/views/settings/Interface/InterfaceWebSshView.vue @@ -66,34 +66,68 @@ const emailAuth = { \ No newline at end of file diff --git a/frontend/src/views/smartcontracts/AddModuleFormView.vue b/frontend/src/views/smartcontracts/AddModuleFormView.vue new file mode 100644 index 0000000..7d6815f --- /dev/null +++ b/frontend/src/views/smartcontracts/AddModuleFormView.vue @@ -0,0 +1,1413 @@ + + + + + + + diff --git a/frontend/src/views/smartcontracts/CreateProposalView.vue b/frontend/src/views/smartcontracts/CreateProposalView.vue index 0b1ef0d..8247351 100644 --- a/frontend/src/views/smartcontracts/CreateProposalView.vue +++ b/frontend/src/views/smartcontracts/CreateProposalView.vue @@ -30,91 +30,29 @@
- -
-
-

Типы операций DLE контракта

-

Выберите тип операции для создания предложения

+ +
+
+ + Для создания предложений необходимо авторизоваться в приложении +

Подключите кошелек в сайдбаре для создания новых предложений

- - -
-
- - Для создания предложений необходимо авторизоваться в приложении -

Подключите кошелек в сайдбаре для создания новых предложений

-
-
- - -
- +
+ + +
+
-
💸 Управление токенами
+
Основные операции DLE
-
💸
Передача токенов

Перевод токенов DLE другому адресу через governance

-
-
- - -
-
🔧 Управление модулями
-
-
-
Добавить модуль
-

Добавление нового модуля в DLE контракт

- -
-
-
-
Удалить модуль
-

Удаление существующего модуля из DLE контракта

- -
-
-
- - -
-
🌐 Управление сетями
-
-
-
-
Добавить сеть
-

Добавление новой поддерживаемой блокчейн сети

- -
-
-
-
Удалить сеть
-

Удаление поддерживаемой блокчейн сети

- -
-
-
- - -
-
⚙️ Настройки DLE
-
-
-
📝
Обновить данные DLE

Изменение основной информации о DLE (название, символ, адрес и т.д.)

-
📊
Изменить кворум

Изменение процента голосов, необходимого для принятия решений

-
Изменить время голосования

Настройка минимального и максимального времени голосования

-
🖼️
+
Оффчейн действие
+

Создание предложения для выполнения оффчейн операций в приложении

+ +
+
+
Добавить модуль
+

Добавление нового модуля в DLE контракт

+ +
+
+
Удалить модуль
+

Удаление существующего модуля из DLE контракта

+ +
+
+
Добавить сеть
+

Добавление новой поддерживаемой блокчейн сети

+ +
+
+
Удалить сеть
+

Удаление поддерживаемой блокчейн сети

+ +
+
Изменить логотип

Обновление URI логотипа DLE для отображения в блокчейн-сканерах

- -
-
📋 Оффчейн операции
-
-
-
📄
-
Оффчейн действие
-

Создание предложения для выполнения оффчейн операций в приложении

- -
-
-
-
@@ -259,7 +212,6 @@ const availableChains = ref([]); // Состояние модулей и их операций const moduleOperations = ref([]); const isLoadingModuleOperations = ref(false); -const modulesWebSocket = ref(null); const isModulesWSConnected = ref(false); // Функции для открытия отдельных форм операций @@ -269,8 +221,11 @@ function openTransferForm() { } function openAddModuleForm() { - // TODO: Открыть форму для добавления модуля - alert('Форма добавления модуля будет реализована'); + if (dleAddress.value) { + router.push(`/management/add-module?address=${dleAddress.value}`); + } else { + router.push('/management/add-module'); + } } function openRemoveModuleForm() { @@ -325,13 +280,7 @@ function openModuleOperationForm(moduleType, operation) { // Получить иконку для типа модуля function getModuleIcon(moduleType) { - const icons = { - treasury: '💰', - timelock: '⏰', - reader: '📖', - hierarchicalVoting: '🗳️' - }; - return icons[moduleType] || '🔧'; + return ''; } // Функции @@ -364,6 +313,9 @@ async function loadDleData() { // Загружаем операции модулей await loadModuleOperations(); + // Повторно подписываемся на обновления модулей для нового DLE + resubscribeToModules(); + } catch (error) { console.error('Ошибка загрузки данных DLE из блокчейна:', error); } finally { @@ -401,49 +353,61 @@ async function loadModuleOperations() { // WebSocket функции для модулей function connectModulesWebSocket() { - if (modulesWebSocket.value && modulesWebSocket.value.readyState === WebSocket.OPEN) { + if (isModulesWSConnected.value) { return; } - const wsUrl = `ws://localhost:8000/ws/deployment`; - modulesWebSocket.value = new WebSocket(wsUrl); - - modulesWebSocket.value.onopen = () => { - console.log('[CreateProposalView] WebSocket модулей соединение установлено'); - isModulesWSConnected.value = true; + try { + // Подключаемся через существующий WebSocket клиент + wsClient.connect(); - // Подписываемся на обновления модулей для текущего DLE - if (dleAddress.value) { - modulesWebSocket.value.send(JSON.stringify({ - type: 'subscribe', - dleAddress: dleAddress.value - })); - } - }; - - modulesWebSocket.value.onmessage = (event) => { - try { - const data = JSON.parse(event.data); + // Подписываемся на события deployment_update + wsClient.on('deployment_update', (data) => { + console.log('[CreateProposalView] Получено обновление деплоя:', data); handleModulesWebSocketMessage(data); - } catch (error) { - console.error('[CreateProposalView] Ошибка парсинга WebSocket сообщения модулей:', error); - } - }; + }); - modulesWebSocket.value.onclose = () => { - console.log('[CreateProposalView] WebSocket модулей соединение закрыто'); + // Подписываемся на подтверждение подписки + wsClient.on('subscribed', (data) => { + console.log('[CreateProposalView] Подписка подтверждена:', data); + }); + + // Подписываемся на обновления модулей + wsClient.on('modules_updated', (data) => { + console.log('[CreateProposalView] Модули обновлены:', data); + // Перезагружаем операции модулей при обновлении + loadModuleOperations(); + }); + + // Подписываемся на статус деплоя + wsClient.on('deployment_status', (data) => { + console.log('[CreateProposalView] Статус деплоя:', data); + handleModulesWebSocketMessage(data); + }); + + // Подписываемся на событие подключения + wsClient.on('connected', () => { + console.log('[CreateProposalView] WebSocket подключен, подписываемся на модули'); + if (dleAddress.value) { + wsClient.ws.send(JSON.stringify({ + type: 'subscribe', + dleAddress: dleAddress.value + })); + console.log('[CreateProposalView] Подписка на модули отправлена для DLE:', dleAddress.value); + } + }); + + isModulesWSConnected.value = true; + console.log('[CreateProposalView] WebSocket модулей соединение установлено'); + } catch (error) { + console.error('[CreateProposalView] Ошибка подключения WebSocket модулей:', error); isModulesWSConnected.value = false; // Переподключаемся через 5 секунд setTimeout(() => { connectModulesWebSocket(); }, 5000); - }; - - modulesWebSocket.value.onerror = (error) => { - console.error('[CreateProposalView] Ошибка WebSocket модулей:', error); - isModulesWSConnected.value = false; - }; + } } function handleModulesWebSocketMessage(data) { @@ -471,10 +435,30 @@ function handleModulesWebSocketMessage(data) { } function disconnectModulesWebSocket() { - if (modulesWebSocket.value) { - modulesWebSocket.value.close(); - modulesWebSocket.value = null; + if (isModulesWSConnected.value) { + // Отписываемся от всех событий + wsClient.off('deployment_update'); + wsClient.off('subscribed'); + wsClient.off('modules_updated'); + wsClient.off('deployment_status'); + wsClient.off('connected'); + isModulesWSConnected.value = false; + console.log('[CreateProposalView] WebSocket модулей отключен'); + } +} + +// Функция для повторной подписки при изменении DLE адреса +function resubscribeToModules() { + if (isModulesWSConnected.value && wsClient.ws && wsClient.ws.readyState === WebSocket.OPEN && dleAddress.value) { + wsClient.ws.send(JSON.stringify({ + type: 'subscribe', + dleAddress: dleAddress.value + })); + console.log('[CreateProposalView] Повторная подписка на модули для DLE:', dleAddress.value); + } else if (wsClient.ws && wsClient.ws.readyState === WebSocket.CONNECTING) { + // Если соединение еще устанавливается, ждем события подключения + console.log('[CreateProposalView] WebSocket еще подключается, ждем события connected'); } } @@ -558,32 +542,6 @@ onUnmounted(() => { color: #333; } -/* Стили для блоков операций */ -.operations-blocks { - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); - border-radius: 12px; - padding: 2rem; - border: 1px solid #e9ecef; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); -} - -.blocks-header { - margin-bottom: 2rem; - text-align: center; -} - -.blocks-header h4 { - color: var(--color-primary); - margin: 0 0 0.5rem 0; - font-size: 1.5rem; - font-weight: 600; -} - -.blocks-header p { - color: #6c757d; - margin: 0; - font-size: 1rem; -} .auth-notice { margin-bottom: 2rem; @@ -626,110 +584,76 @@ onUnmounted(() => { .operation-category h5 { color: var(--color-primary); margin: 0 0 1.5rem 0; - font-size: 1.25rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 0.5rem; + font-size: 1.5rem; + font-weight: 700; padding-bottom: 0.75rem; border-bottom: 2px solid #f0f0f0; + text-align: center; } .operation-blocks { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; } .operation-block { - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); - border: 2px solid #e9ecef; + background: white; border-radius: 12px; - padding: 1.5rem; - text-align: center; + padding: 2rem; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + border: 1px solid #e9ecef; transition: all 0.3s ease; - position: relative; - overflow: hidden; -} - -.operation-block::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, var(--color-primary), #20c997); - transform: scaleX(0); - transition: transform 0.3s ease; + text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 200px; } .operation-block:hover { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); + transform: translateY(-2px); border-color: var(--color-primary); - box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15); - transform: translateY(-4px); } -.operation-block:hover::before { - transform: scaleX(1); -} - -.operation-icon { - font-size: 3rem; - margin-bottom: 1rem; - display: block; -} .operation-block h6 { - color: #333; - margin: 0 0 0.75rem 0; - font-size: 1.1rem; + margin: 0 0 1rem 0; + color: var(--color-primary); + font-size: 1.5rem; font-weight: 600; + flex-shrink: 0; } .operation-block p { - color: #666; margin: 0 0 1.5rem 0; - font-size: 0.9rem; + color: #666; + font-size: 1rem; line-height: 1.5; + flex-grow: 1; } .create-btn { - background: linear-gradient(135deg, var(--color-primary), #20c997); - color: white; + background: var(--color-primary); + color: #fff; border: none; border-radius: 8px; padding: 0.75rem 1.5rem; + cursor: pointer; font-size: 1rem; font-weight: 600; - cursor: pointer; - transition: all 0.3s ease; + transition: all 0.2s; + min-width: 120px; width: 100%; - position: relative; - overflow: hidden; -} - -.create-btn::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.5s ease; + flex-shrink: 0; + margin-top: auto; } .create-btn:hover { - background: linear-gradient(135deg, #0056b3, #1ea085); - transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3); + background: var(--color-primary-dark); + transform: translateY(-1px); } - -.create-btn:hover::before { - left: 100%; -} - .create-btn:disabled { background: #6c757d; cursor: not-allowed; @@ -737,10 +661,6 @@ onUnmounted(() => { box-shadow: none; } -.create-btn:disabled::before { - display: none; -} - /* Стили для модулей */ .module-description { color: #666; @@ -751,54 +671,13 @@ onUnmounted(() => { .module-operation-block { position: relative; - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); - border: 2px solid #e9ecef; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 200px; } -.module-operation-block::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 4px; - background: linear-gradient(90deg, #28a745, #20c997); - transform: scaleX(0); - transition: transform 0.3s ease; -} -.module-operation-block:hover::before { - transform: scaleX(1); -} - -.operation-category-tag { - display: inline-block; - background: linear-gradient(135deg, #28a745, #20c997); - color: white; - padding: 0.25rem 0.75rem; - border-radius: 20px; - font-size: 0.75rem; - font-weight: 600; - margin: 0.5rem 0; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Анимация появления модулей */ -.operation-category { - animation: fadeInUp 0.6s ease-out; -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} /* Индикатор загрузки модулей */ .loading-modules { @@ -828,10 +707,6 @@ onUnmounted(() => { /* Адаптивность */ @media (max-width: 768px) { - .operations-blocks { - padding: 1rem; - } - .operation-blocks { grid-template-columns: 1fr; } @@ -840,14 +715,6 @@ onUnmounted(() => { padding: 1rem; } - .operation-icon { - font-size: 2.5rem; - } - - .blocks-header h4 { - font-size: 1.25rem; - } - .operation-category h5 { font-size: 1.1rem; } diff --git a/frontend/src/views/smartcontracts/DleBlocksManagementView.vue b/frontend/src/views/smartcontracts/DleBlocksManagementView.vue index ec0a256..58a5374 100644 --- a/frontend/src/views/smartcontracts/DleBlocksManagementView.vue +++ b/frontend/src/views/smartcontracts/DleBlocksManagementView.vue @@ -32,42 +32,30 @@
- -
-
+ +
+

Создать предложение

Универсальная форма для создания новых предложений

-
-
-

Предложения

-

Создание, подписание, выполнение

- -
- -
-

Токены DLE

-

Балансы, трансферы, распределение

- -
-
- - -
-
-

Кворум

-

Настройки голосования

- -
-

Модули DLE

Установка, настройка, управление

+
+ + +
+
+

Предложения

+

Создание, подписание, выполнение

+ +

Аналитика

@@ -76,8 +64,8 @@
- -
+ +

История

Лог операций, события, транзакции

@@ -125,21 +113,6 @@ const openProposals = () => { } }; -const openTokens = () => { - if (dleAddress.value) { - router.push(`/management/tokens?address=${dleAddress.value}`); - } else { - router.push('/management/tokens'); - } -}; - -const openQuorum = () => { - if (dleAddress.value) { - router.push(`/management/quorum?address=${dleAddress.value}`); - } else { - router.push('/management/quorum'); - } -}; const openModules = () => { if (dleAddress.value) { @@ -236,15 +209,16 @@ onMounted(() => { } .management-blocks { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: repeat(3, 1fr); gap: 2rem; } -.blocks-row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +.blocks-column { + display: flex; + flex-direction: column; gap: 1.5rem; + align-items: stretch; } .management-block { @@ -255,6 +229,10 @@ onMounted(() => { border: 1px solid #e9ecef; transition: all 0.3s ease; text-align: center; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 250px; } .management-block:hover { @@ -268,6 +246,7 @@ onMounted(() => { color: var(--color-primary); font-size: 1.5rem; font-weight: 600; + flex-shrink: 0; } .management-block p { @@ -275,6 +254,7 @@ onMounted(() => { color: #666; font-size: 1rem; line-height: 1.5; + flex-grow: 1; } .details-btn { @@ -288,6 +268,8 @@ onMounted(() => { font-weight: 600; transition: all 0.2s; min-width: 120px; + flex-shrink: 0; + margin-top: auto; } .details-btn:hover { @@ -295,35 +277,16 @@ onMounted(() => { transform: translateY(-1px); } -/* Стили для блока создания предложения */ -.create-proposal-block { - background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%); - border: 2px solid #28a745; -} - -.create-proposal-block:hover { - border-color: #20c997; - box-shadow: 0 4px 20px rgba(40, 167, 69, 0.15); -} - -.create-proposal-block h3 { - color: #28a745; -} - -.create-btn { - background: linear-gradient(135deg, #28a745, #20c997); - color: white; - font-weight: 700; -} - -.create-btn:hover { - background: linear-gradient(135deg, #218838, #1ea085); - box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); -} /* Адаптивность */ +@media (max-width: 1024px) { + .management-blocks { + grid-template-columns: repeat(2, 1fr); + } +} + @media (max-width: 768px) { - .blocks-row { + .management-blocks { grid-template-columns: 1fr; } diff --git a/frontend/src/views/smartcontracts/DleProposalsView.vue b/frontend/src/views/smartcontracts/DleProposalsView.vue index 071a8df..41edf7d 100644 --- a/frontend/src/views/smartcontracts/DleProposalsView.vue +++ b/frontend/src/views/smartcontracts/DleProposalsView.vue @@ -1,10 +1,8 @@ +
+ + +
- -
-
-

Фильтры

+ +
+
+ Внимание! Для просмотра предложений необходимо авторизоваться.
-
-
- + - - + - + + - -
+
+
+ + +
-
-
-

Предложений пока нет

-
+ +
+
+

Загрузка предложений...

+
-
-
+ +
+
📄
+

Нет предложений

+

Предложения не найдены или еще не загружены

+ +
-
-
{{ getProposalTitle(proposal) }}
- - {{ getProposalStatusText(proposal.status) }} - -
- -
-
- ID: #{{ proposal.id }} -
-
- Создатель: {{ shortenAddress(proposal.initiator) }} -
-
- Создано: {{ formatDate(proposal.blockNumber ? proposal.blockNumber * 1000 : Date.now()) }} -
-
- Цепочка: {{ getChainName(proposal.governanceChainId) || 'Неизвестная сеть' }} -
-
- Дедлайн: {{ formatDate(proposal.deadline) }} -
- - -
-
- Тип модуля: {{ getModuleName(proposal.decodedData.moduleId) }} -
- -
- Сеть: {{ getChainName(proposal.decodedData.chainId) }} -
-
- Длительность: {{ formatDuration(proposal.decodedData.duration) }} + +
+
+
+
Предложение #{{ proposal.id + 1 }}
+
+ {{ getProposalStatusText(proposal.state) }}
-
- Голоса: -
-
- За: {{ formatVotes(proposal.forVotes) }} - Против: {{ formatVotes(proposal.againstVotes) }} -
-
- Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum(proposal) }}% -
-
-
-
-
-
+
{{ proposal.description }}
+ +
+
+ 👤 + Инициатор: {{ proposal.initiator }} +
+
+ 🔗 + ID: {{ proposal.uniqueId }} +
+
+ ⛓️ + Chain: {{ proposal.chainId }} +
+
+ 📄 + Hash: {{ (proposal.transactionHash || '').substring(0, 10) }}...
-
- Операция: - {{ decodeOperation(proposal.operation) }} -
-
- Детали операции: - {{ getOperationDetails(proposal.operation, proposal) }} -
-
- -
- - - - -
- - - Только инициатор предложения может его исполнить - +
+
+
+
+
+ Кворум: {{ getQuorumPercentage(proposal) }}% (требуется: {{ getRequiredQuorumPercentage(proposal) }}%) +
+
+ 👍 За: {{ proposal.forVotes || 0 }} + 👎 Против: {{ proposal.againstVotes || 0 }} + 📊 Всего: {{ (Number(proposal.forVotes || 0) + Number(proposal.againstVotes || 0)) }} +
- -
- - - Для участия в голосовании необходимо авторизоваться - +
+ + + +
-
- - - Для участия в голосовании необходимы права администратора - -
-
- - \ No newline at end of file +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-success { + background: #28a745; + color: white; +} + +.btn-danger { + background: #dc3545; + color: white; +} + +.btn-primary { + background: #007bff; + color: white; +} + +.btn-warning { + background: #ffc107; + color: #333; +} + +@media (max-width: 768px) { + .proposals-page { + padding: 10px; + } + + .proposals-filters { + flex-direction: column; + gap: 10px; + } + + .filter-group input { + min-width: auto; + width: 100%; + } + + .proposals-grid { + grid-template-columns: 1fr; + } +} + \ No newline at end of file diff --git a/frontend/src/views/smartcontracts/QuorumView.vue b/frontend/src/views/smartcontracts/QuorumView.vue deleted file mode 100644 index 3e1125e..0000000 --- a/frontend/src/views/smartcontracts/QuorumView.vue +++ /dev/null @@ -1,598 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/frontend/src/views/smartcontracts/TokensView.vue b/frontend/src/views/smartcontracts/TokensView.vue deleted file mode 100644 index ba318c2..0000000 --- a/frontend/src/views/smartcontracts/TokensView.vue +++ /dev/null @@ -1,740 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/scripts/internal/db/db_init_helper.sh b/scripts/internal/db/db_init_helper.sh index 3b582f4..68d8650 100755 --- a/scripts/internal/db/db_init_helper.sh +++ b/scripts/internal/db/db_init_helper.sh @@ -21,15 +21,25 @@ if [ ! -f "./ssl/keys/full_db_encryption.key" ]; then fi ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key) + +# Создаем роли Read-Only и Editor +docker exec dapp-postgres psql -U dapp_user -d dapp_db -c " +INSERT INTO roles (id, name, description) VALUES + (1, 'readonly', 'Read-Only доступ - только просмотр данных'), + (2, 'editor', 'Editor доступ - просмотр и редактирование данных') +ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description;" + docker exec dapp-postgres psql -U dapp_user -d dapp_db -c " INSERT INTO rpc_providers (network_id_encrypted, rpc_url_encrypted, chain_id) VALUES - (encrypt_text('sepolia', '$ENCRYPTION_KEY'), encrypt_text('https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', '$ENCRYPTION_KEY'), 11155111), + (encrypt_text('sepolia', '$ENCRYPTION_KEY'), encrypt_text('https://1rpc.io/sepolia', '$ENCRYPTION_KEY'), 11155111), (encrypt_text('holesky', '$ENCRYPTION_KEY'), encrypt_text('https://ethereum-holesky.publicnode.com', '$ENCRYPTION_KEY'), 17000) ON CONFLICT DO NOTHING;" docker exec dapp-postgres psql -U dapp_user -d dapp_db -c " -INSERT INTO auth_tokens (name_encrypted, address_encrypted, network_encrypted, min_balance) +INSERT INTO auth_tokens (name_encrypted, address_encrypted, network_encrypted, min_balance, readonly_threshold, editor_threshold) VALUES - (encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('holesky', '$ENCRYPTION_KEY'), 1.0), - (encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('sepolia', '$ENCRYPTION_KEY'), 1.0) + (encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('holesky', '$ENCRYPTION_KEY'), 1.000000000000000000, 1, 1), + (encrypt_text('DLE', '$ENCRYPTION_KEY'), encrypt_text('0x2F2F070AA10bD3Ea14949b9953E2040a05421B17', '$ENCRYPTION_KEY'), encrypt_text('sepolia', '$ENCRYPTION_KEY'), 1.000000000000000000, 1, 1) ON CONFLICT DO NOTHING;"