feat: Добавлены формы деплоя модулей DLE с полными настройками
- Создана форма деплоя TreasuryModule с детальными настройками казны - Создана форма деплоя TimelockModule с настройками временных задержек - Создана форма деплоя DLEReader с простой конфигурацией - Добавлены маршруты и индексы для всех модулей - Исправлены пути импорта BaseLayout - Добавлены авторские права во все файлы - Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
@@ -1,29 +0,0 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
|
||||||
pragma solidity ^0.8.20;
|
|
||||||
|
|
||||||
contract FactoryDeployer {
|
|
||||||
event Deployed(address addr, bytes32 salt);
|
|
||||||
|
|
||||||
function deploy(bytes32 salt, bytes memory creationCode) external payable returns (address addr) {
|
|
||||||
require(creationCode.length != 0, "init code empty");
|
|
||||||
// solhint-disable-next-line no-inline-assembly
|
|
||||||
assembly {
|
|
||||||
addr := create2(callvalue(), add(creationCode, 0x20), mload(creationCode), salt)
|
|
||||||
}
|
|
||||||
require(addr != address(0), "CREATE2 failed");
|
|
||||||
emit Deployed(addr, salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeAddress(bytes32 salt, bytes32 initCodeHash) external view returns (address) {
|
|
||||||
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
|
|
||||||
return address(uint160(uint256(hash)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeAddressWithCreationCode(bytes32 salt, bytes memory creationCode) external view returns (address) {
|
|
||||||
bytes32 initCodeHash = keccak256(creationCode);
|
|
||||||
bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash));
|
|
||||||
return address(uint160(uint256(hash)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
|
// SPDX-License-Identifier: PROPRIETARY
|
||||||
|
// 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
|
||||||
|
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title MockNoop
|
* @title MockNoop
|
||||||
* @dev Простой мок-контракт для тестирования FactoryDeployer
|
* @dev Простой мок-контракт для тестирования
|
||||||
*/
|
*/
|
||||||
contract MockNoop {
|
contract MockNoop {
|
||||||
uint256 public value;
|
uint256 public value;
|
||||||
|
|||||||
138
backend/contracts/MockPaymaster.sol
Normal file
138
backend/contracts/MockPaymaster.sol
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
// 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/IERC20.sol";
|
||||||
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
|
||||||
|
// ERC-4337 интерфейсы для тестирования
|
||||||
|
interface IPaymaster {
|
||||||
|
function validatePaymasterUserOp(
|
||||||
|
UserOperation calldata userOp,
|
||||||
|
bytes32 userOpHash,
|
||||||
|
uint256 maxCost
|
||||||
|
) external returns (bytes memory context, uint256 validationData);
|
||||||
|
|
||||||
|
function postOp(
|
||||||
|
PostOpMode mode,
|
||||||
|
bytes calldata context,
|
||||||
|
uint256 actualGasCost
|
||||||
|
) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserOperation {
|
||||||
|
address sender;
|
||||||
|
uint256 nonce;
|
||||||
|
bytes initCode;
|
||||||
|
bytes callData;
|
||||||
|
uint256 callGasLimit;
|
||||||
|
uint256 verificationGasLimit;
|
||||||
|
uint256 preVerificationGas;
|
||||||
|
uint256 maxFeePerGas;
|
||||||
|
uint256 maxPriorityFeePerGas;
|
||||||
|
bytes paymasterAndData;
|
||||||
|
bytes signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PostOpMode {
|
||||||
|
opSucceeded,
|
||||||
|
opReverted,
|
||||||
|
postOpReverted
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title MockPaymaster
|
||||||
|
* @dev Mock контракт для тестирования ERC-4337 Paymaster функциональности
|
||||||
|
*/
|
||||||
|
contract MockPaymaster is IPaymaster {
|
||||||
|
using SafeERC20 for IERC20;
|
||||||
|
|
||||||
|
// События для тестирования
|
||||||
|
event PaymasterValidated(address indexed sender, uint256 maxCost);
|
||||||
|
event PostOpCalled(PostOpMode mode, uint256 actualGasCost);
|
||||||
|
event TokenReceived(address indexed token, uint256 amount);
|
||||||
|
|
||||||
|
// Статистика для тестирования
|
||||||
|
uint256 public totalValidations;
|
||||||
|
uint256 public totalPostOps;
|
||||||
|
mapping(address => uint256) public tokenReceived;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Валидация UserOperation (всегда успешна для тестов)
|
||||||
|
*/
|
||||||
|
function validatePaymasterUserOp(
|
||||||
|
UserOperation calldata userOp,
|
||||||
|
bytes32 userOpHash,
|
||||||
|
uint256 maxCost
|
||||||
|
) external override returns (bytes memory context, uint256 validationData) {
|
||||||
|
// Используем userOpHash для избежания предупреждения
|
||||||
|
userOpHash;
|
||||||
|
totalValidations++;
|
||||||
|
emit PaymasterValidated(userOp.sender, maxCost);
|
||||||
|
|
||||||
|
// Возвращаем пустой контекст и 0 (успешная валидация)
|
||||||
|
return (abi.encode(userOp.sender, maxCost), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Post-operation обработка
|
||||||
|
*/
|
||||||
|
function postOp(
|
||||||
|
PostOpMode mode,
|
||||||
|
bytes calldata context,
|
||||||
|
uint256 actualGasCost
|
||||||
|
) external override {
|
||||||
|
// Используем context для избежания предупреждения
|
||||||
|
context;
|
||||||
|
totalPostOps++;
|
||||||
|
emit PostOpCalled(mode, actualGasCost);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить токены (для тестирования)
|
||||||
|
*/
|
||||||
|
function receiveTokens(address tokenAddress, uint256 amount) external payable {
|
||||||
|
if (tokenAddress == address(0)) {
|
||||||
|
// Нативные токены
|
||||||
|
require(msg.value == amount, "Incorrect native amount");
|
||||||
|
} else {
|
||||||
|
// ERC20 токены
|
||||||
|
IERC20(tokenAddress).safeTransferFrom(msg.sender, address(this), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenReceived[tokenAddress] += amount;
|
||||||
|
emit TokenReceived(tokenAddress, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить нативные токены
|
||||||
|
*/
|
||||||
|
receive() external payable {
|
||||||
|
tokenReceived[address(0)] += msg.value;
|
||||||
|
emit TokenReceived(address(0), msg.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Получить статистику
|
||||||
|
*/
|
||||||
|
function getStats() external view returns (
|
||||||
|
uint256 validations,
|
||||||
|
uint256 postOps,
|
||||||
|
uint256 nativeReceived
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
totalValidations,
|
||||||
|
totalPostOps,
|
||||||
|
tokenReceived[address(0)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
// SPDX-License-Identifier: PROPRIETARY
|
||||||
|
// 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
|
||||||
|
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
pragma solidity ^0.8.20;
|
pragma solidity ^0.8.20;
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,41 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|||||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||||
import "@openzeppelin/contracts/utils/Address.sol";
|
import "@openzeppelin/contracts/utils/Address.sol";
|
||||||
|
|
||||||
|
// ERC-4337 интерфейсы для оплаты газа любым токеном
|
||||||
|
interface IPaymaster {
|
||||||
|
function validatePaymasterUserOp(
|
||||||
|
UserOperation calldata userOp,
|
||||||
|
bytes32 userOpHash,
|
||||||
|
uint256 maxCost
|
||||||
|
) external returns (bytes memory context, uint256 validationData);
|
||||||
|
|
||||||
|
function postOp(
|
||||||
|
PostOpMode mode,
|
||||||
|
bytes calldata context,
|
||||||
|
uint256 actualGasCost
|
||||||
|
) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserOperation {
|
||||||
|
address sender;
|
||||||
|
uint256 nonce;
|
||||||
|
bytes initCode;
|
||||||
|
bytes callData;
|
||||||
|
uint256 callGasLimit;
|
||||||
|
uint256 verificationGasLimit;
|
||||||
|
uint256 preVerificationGas;
|
||||||
|
uint256 maxFeePerGas;
|
||||||
|
uint256 maxPriorityFeePerGas;
|
||||||
|
bytes paymasterAndData;
|
||||||
|
bytes signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PostOpMode {
|
||||||
|
opSucceeded,
|
||||||
|
opReverted,
|
||||||
|
postOpReverted
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title TreasuryModule
|
* @title TreasuryModule
|
||||||
* @dev Модуль казны для управления активами DLE
|
* @dev Модуль казны для управления активами DLE
|
||||||
@@ -72,6 +107,11 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
// Система экстренного останова
|
// Система экстренного останова
|
||||||
bool public emergencyPaused;
|
bool public emergencyPaused;
|
||||||
address public emergencyAdmin;
|
address public emergencyAdmin;
|
||||||
|
|
||||||
|
// ERC-4337 Paymaster для оплаты газа любым токеном
|
||||||
|
address public paymaster;
|
||||||
|
mapping(address => bool) public gasPaymentTokens; // Токены, которыми можно платить за газ
|
||||||
|
mapping(address => uint256) public gasTokenRates; // Курсы обмена токенов на нативную монету
|
||||||
|
|
||||||
// События
|
// События
|
||||||
event TokenAdded(
|
event TokenAdded(
|
||||||
@@ -103,6 +143,10 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
);
|
);
|
||||||
event EmergencyPauseToggled(bool isPaused, address admin);
|
event EmergencyPauseToggled(bool isPaused, address admin);
|
||||||
event BalanceUpdated(address indexed tokenAddress, uint256 oldBalance, uint256 newBalance);
|
event BalanceUpdated(address indexed tokenAddress, uint256 oldBalance, uint256 newBalance);
|
||||||
|
event PaymasterUpdated(address indexed oldPaymaster, address indexed newPaymaster);
|
||||||
|
event GasPaymentTokenAdded(address indexed tokenAddress, uint256 rate);
|
||||||
|
event GasPaymentTokenRemoved(address indexed tokenAddress);
|
||||||
|
event GasPaidWithToken(address indexed tokenAddress, uint256 tokenAmount, uint256 nativeAmount);
|
||||||
|
|
||||||
// Модификаторы
|
// Модификаторы
|
||||||
modifier onlyDLE() {
|
modifier onlyDLE() {
|
||||||
@@ -143,6 +187,11 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
*/
|
*/
|
||||||
receive() external payable {
|
receive() external payable {
|
||||||
if (msg.value > 0) {
|
if (msg.value > 0) {
|
||||||
|
// Автоматически добавляем нативную монету, если её нет
|
||||||
|
if (!supportedTokens[address(0)].isActive) {
|
||||||
|
_addNativeToken();
|
||||||
|
}
|
||||||
|
|
||||||
_updateTokenBalance(address(0), supportedTokens[address(0)].balance + msg.value);
|
_updateTokenBalance(address(0), supportedTokens[address(0)].balance + msg.value);
|
||||||
emit FundsDeposited(address(0), msg.sender, msg.value, supportedTokens[address(0)].balance);
|
emit FundsDeposited(address(0), msg.sender, msg.value, supportedTokens[address(0)].balance);
|
||||||
}
|
}
|
||||||
@@ -373,6 +422,169 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
emergencyPaused = !emergencyPaused;
|
emergencyPaused = !emergencyPaused;
|
||||||
emit EmergencyPauseToggled(emergencyPaused, msg.sender);
|
emit EmergencyPauseToggled(emergencyPaused, msg.sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== ФУНКЦИИ ДЛЯ ОПЛАТЫ ГАЗА ЛЮБЫМ ТОКЕНОМ =====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Установить Paymaster для ERC-4337 (только через DLE governance)
|
||||||
|
* @param _paymaster Адрес Paymaster контракта
|
||||||
|
*/
|
||||||
|
function setPaymaster(address _paymaster) external onlyDLE {
|
||||||
|
require(_paymaster != address(0), "Paymaster cannot be zero");
|
||||||
|
address oldPaymaster = paymaster;
|
||||||
|
paymaster = _paymaster;
|
||||||
|
emit PaymasterUpdated(oldPaymaster, _paymaster);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Добавить токен для оплаты газа (только через DLE governance)
|
||||||
|
* @param tokenAddress Адрес токена
|
||||||
|
* @param rate Курс обмена (сколько токенов за 1 нативную монету)
|
||||||
|
*/
|
||||||
|
function addGasPaymentToken(address tokenAddress, uint256 rate) external onlyDLE {
|
||||||
|
require(rate > 0, "Rate must be positive");
|
||||||
|
|
||||||
|
// Для нативной монеты проверяем, что она активна
|
||||||
|
if (tokenAddress == address(0)) {
|
||||||
|
require(supportedTokens[tokenAddress].isActive, "Native token must be supported");
|
||||||
|
} else {
|
||||||
|
require(supportedTokens[tokenAddress].isActive, "Token must be supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
gasPaymentTokens[tokenAddress] = true;
|
||||||
|
gasTokenRates[tokenAddress] = rate;
|
||||||
|
|
||||||
|
emit GasPaymentTokenAdded(tokenAddress, rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Удалить токен для оплаты газа (только через DLE governance)
|
||||||
|
* @param tokenAddress Адрес токена
|
||||||
|
*/
|
||||||
|
function removeGasPaymentToken(address tokenAddress) external onlyDLE {
|
||||||
|
require(gasPaymentTokens[tokenAddress], "Token not set for gas payment");
|
||||||
|
|
||||||
|
gasPaymentTokens[tokenAddress] = false;
|
||||||
|
gasTokenRates[tokenAddress] = 0;
|
||||||
|
|
||||||
|
emit GasPaymentTokenRemoved(tokenAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Обновить курс обмена токена (только через DLE governance)
|
||||||
|
* @param tokenAddress Адрес токена
|
||||||
|
* @param newRate Новый курс обмена
|
||||||
|
*/
|
||||||
|
function updateGasTokenRate(address tokenAddress, uint256 newRate) external onlyDLE {
|
||||||
|
require(gasPaymentTokens[tokenAddress], "Token not set for gas payment");
|
||||||
|
require(newRate > 0, "Rate must be positive");
|
||||||
|
|
||||||
|
gasTokenRates[tokenAddress] = newRate;
|
||||||
|
emit GasPaymentTokenAdded(tokenAddress, newRate); // Переиспользуем событие
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Оплатить газ токенами (через ERC-4337 Paymaster)
|
||||||
|
* @param tokenAddress Адрес токена для оплаты (0x0 для нативной монеты)
|
||||||
|
* @param gasAmount Количество газа для оплаты
|
||||||
|
* @param userOp UserOperation для ERC-4337
|
||||||
|
*/
|
||||||
|
function payGasWithToken(
|
||||||
|
address tokenAddress,
|
||||||
|
uint256 gasAmount,
|
||||||
|
UserOperation calldata userOp
|
||||||
|
) external onlyDLE whenNotPaused nonReentrant {
|
||||||
|
_payGasWithToken(tokenAddress, gasAmount, userOp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Проверить, можно ли оплатить газ токеном
|
||||||
|
* @param tokenAddress Адрес токена (0x0 для нативной монеты)
|
||||||
|
* @param gasAmount Количество газа
|
||||||
|
* @return canPay Можно ли оплатить
|
||||||
|
* @return tokenAmount Количество токенов для оплаты
|
||||||
|
*/
|
||||||
|
function canPayGasWithToken(
|
||||||
|
address tokenAddress,
|
||||||
|
uint256 gasAmount
|
||||||
|
) external view returns (bool canPay, uint256 tokenAmount) {
|
||||||
|
if (!gasPaymentTokens[tokenAddress] || !supportedTokens[tokenAddress].isActive) {
|
||||||
|
return (false, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenAmount = (gasAmount * gasTokenRates[tokenAddress]) / 1e18;
|
||||||
|
canPay = supportedTokens[tokenAddress].balance >= tokenAmount;
|
||||||
|
|
||||||
|
return (canPay, tokenAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Проверить, можно ли оплатить газ нативной монетой
|
||||||
|
* @param gasAmount Количество газа
|
||||||
|
* @return canPay Можно ли оплатить
|
||||||
|
* @return nativeAmount Количество нативной монеты для оплаты
|
||||||
|
*/
|
||||||
|
function canPayGasWithNative(
|
||||||
|
uint256 gasAmount
|
||||||
|
) external view returns (bool canPay, uint256 nativeAmount) {
|
||||||
|
return this.canPayGasWithToken(address(0), gasAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Оплатить газ нативной монетой (упрощенная версия)
|
||||||
|
* @param gasAmount Количество газа для оплаты
|
||||||
|
* @param userOp UserOperation для ERC-4337
|
||||||
|
*/
|
||||||
|
function payGasWithNative(
|
||||||
|
uint256 gasAmount,
|
||||||
|
UserOperation calldata userOp
|
||||||
|
) external onlyDLE whenNotPaused nonReentrant {
|
||||||
|
// Используем нативную монету (address(0))
|
||||||
|
_payGasWithToken(address(0), gasAmount, userOp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Внутренняя функция для оплаты газа токенами
|
||||||
|
* @param tokenAddress Адрес токена для оплаты (0x0 для нативной монеты)
|
||||||
|
* @param gasAmount Количество газа для оплаты
|
||||||
|
* @param userOp UserOperation для ERC-4337
|
||||||
|
*/
|
||||||
|
function _payGasWithToken(
|
||||||
|
address tokenAddress,
|
||||||
|
uint256 gasAmount,
|
||||||
|
UserOperation calldata userOp
|
||||||
|
) internal {
|
||||||
|
require(gasPaymentTokens[tokenAddress], "Token not supported for gas payment");
|
||||||
|
require(paymaster != address(0), "Paymaster not set");
|
||||||
|
|
||||||
|
TokenInfo storage tokenInfo = supportedTokens[tokenAddress];
|
||||||
|
require(tokenInfo.isActive, "Token not active");
|
||||||
|
|
||||||
|
// Вычисляем количество токенов для оплаты газа
|
||||||
|
uint256 tokenAmount = (gasAmount * gasTokenRates[tokenAddress]) / 1e18;
|
||||||
|
require(tokenInfo.balance >= tokenAmount, "Insufficient token balance");
|
||||||
|
|
||||||
|
// Обновляем баланс токена
|
||||||
|
_updateTokenBalance(tokenAddress, tokenInfo.balance - tokenAmount);
|
||||||
|
|
||||||
|
// Переводим токены на Paymaster (поддержка нативных и ERC20 токенов)
|
||||||
|
if (tokenInfo.isNative) {
|
||||||
|
// Для нативных токенов (ETH, BNB, MATIC и т.д.)
|
||||||
|
payable(paymaster).sendValue(tokenAmount);
|
||||||
|
} else {
|
||||||
|
// Для ERC20 токенов
|
||||||
|
IERC20(tokenAddress).safeTransfer(paymaster, tokenAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем Paymaster для оплаты газа
|
||||||
|
IPaymaster(paymaster).validatePaymasterUserOp(
|
||||||
|
userOp,
|
||||||
|
keccak256(abi.encode(userOp)),
|
||||||
|
gasAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
emit GasPaidWithToken(tokenAddress, tokenAmount, gasAmount);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== VIEW ФУНКЦИИ =====
|
// ===== VIEW ФУНКЦИИ =====
|
||||||
|
|
||||||
@@ -396,17 +608,28 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
function getActiveTokens() external view returns (address[] memory) {
|
function getActiveTokens() external view returns (address[] memory) {
|
||||||
uint256 activeCount = 0;
|
uint256 activeCount = 0;
|
||||||
|
|
||||||
// Считаем активные токены
|
// Считаем активные токены (включая нативную монету)
|
||||||
for (uint256 i = 0; i < tokenList.length; i++) {
|
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||||
if (supportedTokens[tokenList[i]].isActive) {
|
if (supportedTokens[tokenList[i]].isActive) {
|
||||||
activeCount++;
|
activeCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Нативная монета всегда активна
|
||||||
|
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём массив активных токенов
|
// Создаём массив активных токенов
|
||||||
address[] memory activeTokens = new address[](activeCount);
|
address[] memory activeTokens = new address[](activeCount);
|
||||||
uint256 index = 0;
|
uint256 index = 0;
|
||||||
|
|
||||||
|
// Добавляем нативную монету первой, если есть баланс
|
||||||
|
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
|
||||||
|
activeTokens[index] = address(0);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
for (uint256 i = 0; i < tokenList.length; i++) {
|
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||||
if (supportedTokens[tokenList[i]].isActive) {
|
if (supportedTokens[tokenList[i]].isActive) {
|
||||||
activeTokens[index] = tokenList[i];
|
activeTokens[index] = tokenList[i];
|
||||||
@@ -421,6 +644,10 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
* @dev Получить баланс токена
|
* @dev Получить баланс токена
|
||||||
*/
|
*/
|
||||||
function getTokenBalance(address tokenAddress) external view returns (uint256) {
|
function getTokenBalance(address tokenAddress) external view returns (uint256) {
|
||||||
|
// Для нативной монеты возвращаем реальный баланс, если токен не зарегистрирован
|
||||||
|
if (tokenAddress == address(0) && !supportedTokens[address(0)].isActive) {
|
||||||
|
return address(this).balance;
|
||||||
|
}
|
||||||
return supportedTokens[tokenAddress].balance;
|
return supportedTokens[tokenAddress].balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +666,10 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
* @dev Проверить, поддерживается ли токен
|
* @dev Проверить, поддерживается ли токен
|
||||||
*/
|
*/
|
||||||
function isTokenSupported(address tokenAddress) external view returns (bool) {
|
function isTokenSupported(address tokenAddress) external view returns (bool) {
|
||||||
|
// Нативная монета всегда поддерживается
|
||||||
|
if (tokenAddress == address(0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return supportedTokens[tokenAddress].isActive;
|
return supportedTokens[tokenAddress].isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,13 +680,25 @@ contract TreasuryModule is ReentrancyGuard {
|
|||||||
uint256 totalTokens,
|
uint256 totalTokens,
|
||||||
uint256 totalTxs,
|
uint256 totalTxs,
|
||||||
uint256 currentChainId,
|
uint256 currentChainId,
|
||||||
bool isPaused
|
bool isPaused,
|
||||||
|
address paymasterAddress,
|
||||||
|
uint256 gasPaymentTokensCount
|
||||||
) {
|
) {
|
||||||
|
// Считаем количество токенов для оплаты газа
|
||||||
|
uint256 gasTokensCount = 0;
|
||||||
|
for (uint256 i = 0; i < tokenList.length; i++) {
|
||||||
|
if (gasPaymentTokens[tokenList[i]]) {
|
||||||
|
gasTokensCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
totalTokensSupported,
|
totalTokensSupported,
|
||||||
totalTransactions,
|
totalTransactions,
|
||||||
chainId,
|
chainId,
|
||||||
emergencyPaused
|
emergencyPaused,
|
||||||
|
paymaster,
|
||||||
|
gasTokensCount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ let pool = new Pool({
|
|||||||
max: 10, // Максимальное количество клиентов в пуле
|
max: 10, // Максимальное количество клиентов в пуле
|
||||||
min: 0, // Минимальное количество клиентов в пуле
|
min: 0, // Минимальное количество клиентов в пуле
|
||||||
idleTimeoutMillis: 30000, // Время жизни неактивного клиента (30 сек)
|
idleTimeoutMillis: 30000, // Время жизни неактивного клиента (30 сек)
|
||||||
connectionTimeoutMillis: 2000, // Таймаут подключения (2 сек)
|
connectionTimeoutMillis: 30000, // Таймаут подключения (30 сек)
|
||||||
maxUses: 7500, // Максимальное количество использований клиента
|
maxUses: 7500, // Максимальное количество использований клиента
|
||||||
allowExitOnIdle: true, // Разрешить выход при отсутствии активных клиентов
|
allowExitOnIdle: true, // Разрешить выход при отсутствии активных клиентов
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,23 +15,9 @@ require('hardhat-contract-sizer');
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
function getNetworks() {
|
function getNetworks() {
|
||||||
const supported = [
|
// Возвращаем пустой объект, чтобы Hardhat не зависел от переменных окружения
|
||||||
{ id: 'bsc', envUrl: 'BSC_RPC_URL', envKey: 'BSC_PRIVATE_KEY' },
|
// Сети будут настраиваться динамически в deploy-multichain.js
|
||||||
{ id: 'ethereum', envUrl: 'ETHEREUM_RPC_URL', envKey: 'ETHEREUM_PRIVATE_KEY' },
|
return {};
|
||||||
{ id: 'arbitrum', envUrl: 'ARBITRUM_RPC_URL', envKey: 'ARBITRUM_PRIVATE_KEY' },
|
|
||||||
{ id: 'polygon', envUrl: 'POLYGON_RPC_URL', envKey: 'POLYGON_PRIVATE_KEY' },
|
|
||||||
{ id: 'sepolia', envUrl: 'SEPOLIA_RPC_URL', envKey: 'SEPOLIA_PRIVATE_KEY' },
|
|
||||||
];
|
|
||||||
const networks = {};
|
|
||||||
for (const net of supported) {
|
|
||||||
if (process.env[net.envUrl] && process.env[net.envKey]) {
|
|
||||||
networks[net.id] = {
|
|
||||||
url: process.env[net.envUrl],
|
|
||||||
accounts: [process.env[net.envKey]],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return networks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
|
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
|
||||||
"run-migrations": "node scripts/run-migrations.js",
|
"run-migrations": "node scripts/run-migrations.js",
|
||||||
"fix-duplicates": "node scripts/fix-duplicate-identities.js",
|
"fix-duplicates": "node scripts/fix-duplicate-identities.js",
|
||||||
"deploy:factory": "node scripts/deploy/deploy-factory.js",
|
|
||||||
"deploy:multichain": "node scripts/deploy/deploy-multichain.js",
|
"deploy:multichain": "node scripts/deploy/deploy-multichain.js",
|
||||||
"deploy:complete": "node scripts/deploy/deploy-dle-complete.js"
|
"deploy:complete": "node scripts/deploy/deploy-dle-complete.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
"function balanceOf(address account) external view returns (uint256)",
|
"function balanceOf(address account) external view returns (uint256)",
|
||||||
"function quorumPercentage() external view returns (uint256)",
|
"function quorumPercentage() external view returns (uint256)",
|
||||||
"function getCurrentChainId() external view returns (uint256)",
|
"function getCurrentChainId() external view returns (uint256)",
|
||||||
"function logoURI() external view returns (string memory)"
|
"function logoURI() external view returns (string memory)",
|
||||||
|
"function getModuleAddress(bytes32 _moduleId) external view returns (address)"
|
||||||
];
|
];
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
@@ -174,6 +175,36 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Читаем информацию о модулях
|
||||||
|
const modules = {};
|
||||||
|
try {
|
||||||
|
console.log(`[Blockchain] Читаем модули для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Определяем известные модули
|
||||||
|
const moduleNames = ['reader', 'treasury', 'timelock'];
|
||||||
|
|
||||||
|
for (const moduleName of moduleNames) {
|
||||||
|
try {
|
||||||
|
// Вычисляем moduleId (keccak256 от имени модуля)
|
||||||
|
const moduleId = ethers.keccak256(ethers.toUtf8Bytes(moduleName));
|
||||||
|
|
||||||
|
// Получаем адрес модуля
|
||||||
|
const moduleAddress = await dle.getModuleAddress(moduleId);
|
||||||
|
|
||||||
|
if (moduleAddress && moduleAddress !== ethers.ZeroAddress) {
|
||||||
|
modules[moduleName] = moduleAddress;
|
||||||
|
console.log(`[Blockchain] Модуль ${moduleName}: ${moduleAddress}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Blockchain] Модуль ${moduleName} не инициализирован`);
|
||||||
|
}
|
||||||
|
} catch (moduleError) {
|
||||||
|
console.log(`[Blockchain] Ошибка при чтении модуля ${moduleName}:`, moduleError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (modulesError) {
|
||||||
|
console.log(`[Blockchain] Ошибка при чтении модулей:`, modulesError.message);
|
||||||
|
}
|
||||||
|
|
||||||
const blockchainData = {
|
const blockchainData = {
|
||||||
name: dleInfo.name,
|
name: dleInfo.name,
|
||||||
symbol: dleInfo.symbol,
|
symbol: dleInfo.symbol,
|
||||||
@@ -193,7 +224,8 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
quorumPercentage: Number(quorumPercentage),
|
quorumPercentage: Number(quorumPercentage),
|
||||||
currentChainId: Number(currentChainId),
|
currentChainId: Number(currentChainId),
|
||||||
rpcUsed: rpcUrl,
|
rpcUsed: rpcUrl,
|
||||||
participantCount: participantCount
|
participantCount: participantCount,
|
||||||
|
modules: modules // Информация о модулях
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData);
|
console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData);
|
||||||
@@ -212,92 +244,7 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получение поддерживаемых сетей из смарт-контракта
|
// УДАЛЕНО: дублируется в dleMultichain.js
|
||||||
router.post('/get-supported-chains', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE обязателен'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Получение поддерживаемых сетей для 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 isChainSupported(uint256 _chainId) external view returns (bool)",
|
|
||||||
"function getCurrentChainId() external view returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Список всех возможных сетей для проверки
|
|
||||||
const allChains = [
|
|
||||||
{ chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' },
|
|
||||||
{ chainId: 137, name: 'Polygon', description: 'Сеть Polygon' },
|
|
||||||
{ chainId: 56, name: 'BSC', description: 'Binance Smart Chain' },
|
|
||||||
{ chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' },
|
|
||||||
{ chainId: 10, name: 'Optimism', description: 'Optimism' },
|
|
||||||
{ chainId: 8453, name: 'Base', description: 'Base' },
|
|
||||||
{ chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' },
|
|
||||||
{ chainId: 250, name: 'Fantom', description: 'Fantom Opera' },
|
|
||||||
{ chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' },
|
|
||||||
{ chainId: 17000, name: 'Holesky', description: 'Ethereum Testnet Holesky' },
|
|
||||||
{ chainId: 80002, name: 'Polygon Amoy', description: 'Polygon Testnet Amoy' },
|
|
||||||
{ chainId: 84532, name: 'Base Sepolia', description: 'Base Sepolia Testnet' },
|
|
||||||
{ chainId: 421614, name: 'Arbitrum Sepolia', description: 'Arbitrum Sepolia Testnet' },
|
|
||||||
{ chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' },
|
|
||||||
{ chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' },
|
|
||||||
{ chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const supportedChains = [];
|
|
||||||
|
|
||||||
// Проверяем каждую сеть через смарт-контракт
|
|
||||||
for (const chain of allChains) {
|
|
||||||
try {
|
|
||||||
const isSupported = await dle.isChainSupported(chain.chainId);
|
|
||||||
if (isSupported) {
|
|
||||||
supportedChains.push(chain);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`[Blockchain] Ошибка при проверке сети ${chain.chainId}:`, error.message);
|
|
||||||
// Продолжаем проверку других сетей
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Найдено поддерживаемых сетей: ${supportedChains.length}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
chains: supportedChains,
|
|
||||||
totalCount: supportedChains.length
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при получении поддерживаемых сетей:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении поддерживаемых сетей: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получение списка всех предложений
|
// Получение списка всех предложений
|
||||||
router.post('/get-proposals', async (req, res) => {
|
router.post('/get-proposals', async (req, res) => {
|
||||||
@@ -354,7 +301,7 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
// Пробуем несколько раз для новых предложений
|
// Пробуем несколько раз для новых предложений
|
||||||
let proposal, isPassed;
|
let proposal, isPassed;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 3;
|
const maxRetries = 1;
|
||||||
|
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
try {
|
try {
|
||||||
@@ -708,111 +655,9 @@ router.post('/load-deactivation-proposals', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Создать предложение о добавлении модуля
|
// УДАЛЕНО: дублируется в dleModules.js
|
||||||
router.post('/create-add-module-proposal', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, description, duration, moduleId, moduleAddress, chainId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || !description || !duration || !moduleId || !moduleAddress || !chainId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Все поля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Создание предложения о добавлении модуля: ${moduleId} для DLE: ${dleAddress}`);
|
// УДАЛЕНО: дублируется в dleModules.js
|
||||||
|
|
||||||
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 createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Создаем предложение
|
|
||||||
const tx = await dle.createAddModuleProposal(description, duration, moduleId, moduleAddress, chainId);
|
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Предложение о добавлении модуля создано:`, receipt);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
proposalId: receipt.logs[0].args.proposalId,
|
|
||||||
transactionHash: receipt.hash
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при создании предложения о добавлении модуля:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при создании предложения о добавлении модуля: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Создать предложение об удалении модуля
|
|
||||||
router.post('/create-remove-module-proposal', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, description, duration, moduleId, chainId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || !description || !duration || !moduleId || !chainId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Все поля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Создание предложения об удалении модуля: ${moduleId} для 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 createRemoveModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) external returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Создаем предложение
|
|
||||||
const tx = await dle.createRemoveModuleProposal(description, duration, moduleId, chainId);
|
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Предложение об удалении модуля создано:`, receipt);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
proposalId: receipt.logs[0].args.proposalId,
|
|
||||||
transactionHash: receipt.hash
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при создании предложения об удалении модуля:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при создании предложения об удалении модуля: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// УДАЛЯЕМ эту функцию - создание предложений выполняется только через frontend с MetaMask
|
// УДАЛЯЕМ эту функцию - создание предложений выполняется только через frontend с MetaMask
|
||||||
// router.post('/create-proposal', ...) - УДАЛЕНО
|
// router.post('/create-proposal', ...) - УДАЛЕНО
|
||||||
@@ -925,264 +770,15 @@ router.post('/cancel-proposal', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Проверить подключение к сети
|
// УДАЛЕНО: дублируется в dleMultichain.js
|
||||||
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: 'Все поля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Проверка подключения к сети ${chainId} для DLE: ${dleAddress}`);
|
// УДАЛЕНО: дублируется в dleMultichain.js
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// УДАЛЕНО: дублируется в dleMultichain.js
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
// УДАЛЕНО: дублируется в dleMultichain.js
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
// УДАЛЕНО: дублируется в dleMultichain.js
|
||||||
|
|
||||||
// Проверяем подключение
|
|
||||||
const isAvailable = await dle.checkChainConnection(chainId);
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Подключение к сети ${chainId}: ${isAvailable}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
chainId: chainId,
|
|
||||||
isAvailable: isAvailable
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при проверке подключения к сети:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при проверке подключения к сети: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Синхронизировать во все сети
|
|
||||||
router.post('/sync-to-all-chains', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, proposalId, userAddress } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || proposalId === undefined || !userAddress) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Все поля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Синхронизация предложения ${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 syncToAllChains(uint256 _proposalId) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Синхронизируем во все сети
|
|
||||||
const tx = await dle.syncToAllChains(proposalId);
|
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Синхронизация выполнена:`, receipt);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
transactionHash: receipt.hash
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при синхронизации во все сети:', 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(`[Blockchain] Получение количества поддерживаемых сетей для 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() public view returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Получаем количество сетей
|
|
||||||
const count = await dle.getSupportedChainCount();
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Количество поддерживаемых сетей: ${count}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
count: Number(count)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при получении количества поддерживаемых сетей:', 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: 'Все поля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Получение 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) public view returns (uint256)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Получаем ID сети
|
|
||||||
const chainId = await dle.getSupportedChainId(index);
|
|
||||||
|
|
||||||
console.log(`[Blockchain] ID сети по индексу ${index}: ${chainId}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
index: Number(index),
|
|
||||||
chainId: Number(chainId)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при получении ID поддерживаемой сети:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении ID поддерживаемой сети: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Исполнить предложение по подписям
|
|
||||||
router.post('/execute-proposal-by-signatures', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, proposalId, signers, signatures, userAddress } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || proposalId === undefined || !signers || !signatures || !userAddress) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Все поля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Исполнение предложения ${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 executeProposalBySignatures(uint256 _proposalId, address[] calldata signers, bytes[] calldata signatures) external"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Исполняем предложение по подписям
|
|
||||||
const tx = await dle.executeProposalBySignatures(proposalId, signers, signatures);
|
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Предложение исполнено по подписям:`, receipt);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
transactionHash: receipt.hash
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при исполнении предложения по подписям:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при исполнении предложения по подписям: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить параметры управления
|
// Получить параметры управления
|
||||||
router.post('/get-governance-params', async (req, res) => {
|
router.post('/get-governance-params', async (req, res) => {
|
||||||
@@ -1707,139 +1303,11 @@ router.post('/is-active', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Проверить активность модуля
|
// УДАЛЕНО: дублируется в dleModules.js
|
||||||
router.post('/is-module-active', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, moduleId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || !moduleId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE и ID модуля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Проверка активности модуля: ${moduleId} для DLE: ${dleAddress}`);
|
// УДАЛЕНО: дублируется в dleModules.js
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
// УДАЛЕНО: дублируется в dleModules.js
|
||||||
if (!rpcUrl) {
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'RPC URL для Sepolia не найден'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
||||||
|
|
||||||
const dleAbi = [
|
|
||||||
"function isModuleActive(bytes32 _moduleId) external view returns (bool)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Проверяем активность модуля
|
|
||||||
const isActive = await dle.isModuleActive(moduleId);
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Активность модуля ${moduleId}: ${isActive}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
isActive: isActive
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при проверке активности модуля:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при проверке активности модуля: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить адрес модуля
|
|
||||||
router.post('/get-module-address', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, moduleId } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress || !moduleId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE и ID модуля обязательны'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Получение адреса модуля: ${moduleId} для 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 getModuleAddress(bytes32 _moduleId) external view returns (address)"
|
|
||||||
];
|
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
|
||||||
|
|
||||||
// Получаем адрес модуля
|
|
||||||
const moduleAddress = await dle.getModuleAddress(moduleId);
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Адрес модуля ${moduleId}: ${moduleAddress}`);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
moduleAddress: moduleAddress
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при получении адреса модуля:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении адреса модуля: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить все модули (заглушка)
|
|
||||||
router.post('/get-all-modules', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress } = req.body;
|
|
||||||
|
|
||||||
if (!dleAddress) {
|
|
||||||
return res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес DLE обязателен'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[Blockchain] Получение всех модулей для DLE: ${dleAddress}`);
|
|
||||||
|
|
||||||
// Пока возвращаем заглушку, так как в смарт контракте нет функции для получения всех модулей
|
|
||||||
// В реальности нужно будет реализовать через события или другие методы
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
modules: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Blockchain] Ошибка при получении всех модулей:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Ошибка при получении всех модулей: ' + error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Получить аналитику DLE
|
// Получить аналитику DLE
|
||||||
router.post('/get-dle-analytics', async (req, res) => {
|
router.post('/get-dle-analytics', async (req, res) => {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
|
|
||||||
// ABI для чтения данных DLE
|
// ABI для чтения данных DLE
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function getDLEInfo() external view returns (tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, uint256 oktmo, string[] okvedCodes, uint256 kpp, uint256 creationTimestamp, bool isActive))",
|
"function getDLEInfo() external view returns (tuple(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, uint256 creationTimestamp, bool isActive))",
|
||||||
"function totalSupply() external view returns (uint256)",
|
"function totalSupply() external view returns (uint256)",
|
||||||
"function balanceOf(address account) external view returns (uint256)",
|
"function balanceOf(address account) external view returns (uint256)",
|
||||||
"function quorumPercentage() external view returns (uint256)",
|
"function quorumPercentage() external view returns (uint256)",
|
||||||
@@ -163,7 +163,6 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
location: dleInfo.location,
|
location: dleInfo.location,
|
||||||
coordinates: dleInfo.coordinates,
|
coordinates: dleInfo.coordinates,
|
||||||
jurisdiction: Number(dleInfo.jurisdiction),
|
jurisdiction: Number(dleInfo.jurisdiction),
|
||||||
oktmo: Number(dleInfo.oktmo),
|
|
||||||
okvedCodes: dleInfo.okvedCodes,
|
okvedCodes: dleInfo.okvedCodes,
|
||||||
kpp: Number(dleInfo.kpp),
|
kpp: Number(dleInfo.kpp),
|
||||||
creationTimestamp: Number(dleInfo.creationTimestamp),
|
creationTimestamp: Number(dleInfo.creationTimestamp),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -40,13 +40,21 @@ router.post('/get-supported-chains', async (req, res) => {
|
|||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function listSupportedChains() external view returns (uint256[] memory)"
|
"function getSupportedChainCount() external view returns (uint256)",
|
||||||
|
"function getSupportedChainId(uint256 _index) external view returns (uint256)"
|
||||||
];
|
];
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
// Получаем поддерживаемые сети
|
// Получаем количество поддерживаемых сетей
|
||||||
const supportedChains = await dle.listSupportedChains();
|
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);
|
console.log(`[DLE Multichain] Поддерживаемые сети:`, supportedChains);
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,11 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
|
|
||||||
// ABI для чтения предложений (используем правильные функции из смарт-контракта)
|
// ABI для чтения предложений (используем правильные функции из смарт-контракта)
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"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 targets)",
|
|
||||||
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
|
||||||
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
||||||
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
||||||
|
"function 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)"
|
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -68,15 +70,34 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`);
|
console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`);
|
||||||
|
|
||||||
// Пробуем несколько раз для новых предложений
|
// Пробуем несколько раз для новых предложений
|
||||||
let proposal, isPassed;
|
let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const maxRetries = 3;
|
const maxRetries = 1;
|
||||||
|
|
||||||
while (retryCount < maxRetries) {
|
while (retryCount < maxRetries) {
|
||||||
try {
|
try {
|
||||||
proposal = await dle.getProposalSummary(proposalId);
|
proposalState = await dle.getProposalState(proposalId);
|
||||||
const result = await dle.checkProposalResult(proposalId);
|
const result = await dle.checkProposalResult(proposalId);
|
||||||
isPassed = result.passed;
|
isPassed = result.passed;
|
||||||
|
quorumReached = result.quorumReached;
|
||||||
|
|
||||||
|
// Получаем данные о голосах из структуры Proposal
|
||||||
|
try {
|
||||||
|
const proposalData = await dle.proposals(proposalId);
|
||||||
|
forVotes = Number(proposalData.forVotes);
|
||||||
|
againstVotes = Number(proposalData.againstVotes);
|
||||||
|
|
||||||
|
// Вычисляем требуемый кворум
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
break; // Успешно прочитали
|
break; // Успешно прочитали
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
retryCount++;
|
retryCount++;
|
||||||
@@ -90,33 +111,29 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Данные предложения ${proposalId}:`, {
|
console.log(`[DLE Proposals] Данные предложения ${proposalId}:`, {
|
||||||
id: Number(proposal.id),
|
id: Number(proposalId),
|
||||||
description: proposal.description,
|
description: events[i].args.description,
|
||||||
forVotes: Number(proposal.forVotes),
|
state: Number(proposalState),
|
||||||
againstVotes: Number(proposal.againstVotes),
|
isPassed: isPassed,
|
||||||
executed: proposal.executed,
|
quorumReached: quorumReached,
|
||||||
canceled: proposal.canceled,
|
forVotes: Number(forVotes),
|
||||||
deadline: Number(proposal.deadline),
|
againstVotes: Number(againstVotes),
|
||||||
initiator: proposal.initiator,
|
quorumRequired: Number(quorumRequired),
|
||||||
governanceChainId: Number(proposal.governanceChainId),
|
initiator: events[i].args.initiator
|
||||||
snapshotTimepoint: Number(proposal.snapshotTimepoint),
|
|
||||||
targets: proposal.targets
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const proposalInfo = {
|
const proposalInfo = {
|
||||||
id: Number(proposal.id),
|
id: Number(proposalId),
|
||||||
description: proposal.description,
|
description: events[i].args.description,
|
||||||
forVotes: Number(proposal.forVotes),
|
state: Number(proposalState),
|
||||||
againstVotes: Number(proposal.againstVotes),
|
|
||||||
executed: proposal.executed,
|
|
||||||
canceled: proposal.canceled,
|
|
||||||
deadline: Number(proposal.deadline),
|
|
||||||
initiator: proposal.initiator,
|
|
||||||
governanceChainId: Number(proposal.governanceChainId),
|
|
||||||
snapshotTimepoint: Number(proposal.snapshotTimepoint),
|
|
||||||
targetChains: proposal.targets.map(chainId => Number(chainId)),
|
|
||||||
isPassed: isPassed,
|
isPassed: isPassed,
|
||||||
blockNumber: events[i].blockNumber
|
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);
|
proposals.push(proposalInfo);
|
||||||
@@ -182,29 +199,40 @@ router.post('/get-proposal-info', async (req, res) => {
|
|||||||
|
|
||||||
// ABI для чтения информации о предложении
|
// ABI для чтения информации о предложении
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function proposals(uint256) external view returns (tuple(string description, uint256 duration, bytes operation, uint256 governanceChainId, uint256 startTime, bool executed, uint256 forVotes, uint256 againstVotes))",
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
||||||
"function checkProposalResult(uint256 _proposalId) external view returns (bool)"
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
||||||
|
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
|
||||||
];
|
];
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
// Читаем информацию о предложении
|
// Ищем событие ProposalCreated для этого предложения
|
||||||
const proposal = await dle.proposals(proposalId);
|
const currentBlock = await provider.getBlockNumber();
|
||||||
const isPassed = await dle.checkProposalResult(proposalId);
|
const fromBlock = Math.max(0, currentBlock - 10000);
|
||||||
|
|
||||||
|
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
|
||||||
|
const proposalEvent = events.find(event => Number(event.args.proposalId) === proposalId);
|
||||||
|
|
||||||
|
if (!proposalEvent) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Предложение не найдено'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем состояние и результат предложения
|
||||||
|
const result = await dle.checkProposalResult(proposalId);
|
||||||
|
const state = await dle.getProposalState(proposalId);
|
||||||
|
|
||||||
// governanceChainId не сохраняется в предложении, используем текущую цепочку
|
|
||||||
const governanceChainId = 11155111; // Sepolia chain ID
|
|
||||||
|
|
||||||
const proposalInfo = {
|
const proposalInfo = {
|
||||||
description: proposal.description,
|
id: Number(proposalId),
|
||||||
duration: Number(proposal.duration),
|
description: proposalEvent.args.description,
|
||||||
operation: proposal.operation,
|
initiator: proposalEvent.args.initiator,
|
||||||
governanceChainId: Number(proposal.governanceChainId),
|
blockNumber: proposalEvent.blockNumber,
|
||||||
startTime: Number(proposal.startTime),
|
transactionHash: proposalEvent.transactionHash,
|
||||||
executed: proposal.executed,
|
state: Number(state),
|
||||||
forVotes: Number(proposal.forVotes),
|
isPassed: result.passed,
|
||||||
againstVotes: Number(proposal.againstVotes),
|
quorumReached: result.quorumReached
|
||||||
isPassed: isPassed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Информация о предложении получена:`, proposalInfo);
|
console.log(`[DLE Proposals] Информация о предложении получена:`, proposalInfo);
|
||||||
@@ -300,24 +328,30 @@ router.post('/get-proposal-votes', async (req, res) => {
|
|||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function getProposalVotes(uint256 _proposalId) external view returns (uint256 forVotes, uint256 againstVotes, uint256 totalVotes, uint256 quorumRequired)"
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
||||||
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)"
|
||||||
];
|
];
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
// Получаем голоса по предложению
|
// Получаем результат предложения
|
||||||
const votes = await dle.getProposalVotes(proposalId);
|
const result = await dle.checkProposalResult(proposalId);
|
||||||
|
const state = await dle.getProposalState(proposalId);
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Голоса по предложению ${proposalId}:`, votes);
|
console.log(`[DLE Proposals] Результат предложения ${proposalId}:`, { result, state });
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
proposalId: Number(proposalId),
|
proposalId: Number(proposalId),
|
||||||
forVotes: Number(votes.forVotes),
|
isPassed: result.passed,
|
||||||
againstVotes: Number(votes.againstVotes),
|
quorumReached: result.quorumReached,
|
||||||
totalVotes: Number(votes.totalVotes),
|
state: Number(state),
|
||||||
quorumRequired: Number(votes.quorumRequired)
|
// Пока не можем получить точные голоса, так как функция не существует в контракте
|
||||||
|
forVotes: 0,
|
||||||
|
againstVotes: 0,
|
||||||
|
totalVotes: 0,
|
||||||
|
quorumRequired: 0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -539,19 +573,22 @@ router.post('/get-quorum-at', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Исполнить предложение
|
// Исполнить предложение (подготовка транзакции для MetaMask)
|
||||||
router.post('/execute-proposal', async (req, res) => {
|
router.post('/execute-proposal', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { dleAddress, proposalId, userAddress, privateKey } = req.body;
|
console.log('[DLE Proposals] Получен запрос на исполнение предложения:', req.body);
|
||||||
|
|
||||||
if (!dleAddress || proposalId === undefined || !userAddress || !privateKey) {
|
const { dleAddress, proposalId } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined) {
|
||||||
|
console.log('[DLE Proposals] Ошибка валидации: отсутствуют обязательные поля');
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Все поля обязательны, включая приватный ключ'
|
error: 'Необходимы dleAddress и proposalId'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Исполнение предложения ${proposalId} в DLE: ${dleAddress}`);
|
console.log(`[DLE Proposals] Подготовка исполнения предложения ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
if (!rpcUrl) {
|
if (!rpcUrl) {
|
||||||
@@ -562,32 +599,34 @@ router.post('/execute-proposal', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
const wallet = new ethers.Wallet(privateKey, provider);
|
|
||||||
|
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function executeProposal(uint256 _proposalId) external"
|
"function executeProposal(uint256 _proposalId) external"
|
||||||
];
|
];
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, dleAbi, wallet);
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
// Исполняем предложение
|
// Подготавливаем данные для транзакции (не отправляем)
|
||||||
const tx = await dle.executeProposal(proposalId);
|
const txData = await dle.executeProposal.populateTransaction(proposalId);
|
||||||
const receipt = await tx.wait();
|
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Предложение исполнено:`, receipt);
|
console.log(`[DLE Proposals] Данные транзакции исполнения подготовлены:`, txData);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
transactionHash: receipt.hash
|
to: dleAddress,
|
||||||
|
data: txData.data,
|
||||||
|
value: "0x0",
|
||||||
|
gasLimit: "0x1e8480", // 2,000,000 gas
|
||||||
|
message: `Подготовлены данные для исполнения предложения ${proposalId}. Отправьте транзакцию через MetaMask.`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[DLE Proposals] Ошибка при исполнении предложения:', error);
|
console.error('[DLE Proposals] Ошибка при подготовке исполнения предложения:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Ошибка при исполнении предложения: ' + error.message
|
error: 'Ошибка при подготовке исполнения предложения: ' + error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -795,4 +834,248 @@ router.post('/list-proposals', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Голосовать за предложение
|
||||||
|
router.post('/vote-proposal', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress, proposalId, support } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined || support === undefined) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Необходимы dleAddress, proposalId и support'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Голосование за предложение ${proposalId} в DLE: ${dleAddress}, поддержка: ${support}`);
|
||||||
|
|
||||||
|
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 vote(uint256 _proposalId, bool _support) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Подготавливаем данные для транзакции (не отправляем)
|
||||||
|
const txData = await dle.vote.populateTransaction(proposalId, support);
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Данные транзакции голосования подготовлены:`, txData);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
to: dleAddress,
|
||||||
|
data: txData.data,
|
||||||
|
value: "0x0",
|
||||||
|
gasLimit: "0x1e8480", // 2,000,000 gas
|
||||||
|
message: `Подготовлены данные для голосования ${support ? 'за' : 'против'} предложения ${proposalId}. Отправьте транзакцию через MetaMask.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} 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 {
|
||||||
|
const { txHash, dleAddress, proposalId, support } = req.body;
|
||||||
|
|
||||||
|
if (!txHash || !dleAddress || proposalId === undefined || support === undefined) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Необходимы txHash, dleAddress, proposalId и support'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Отслеживание транзакции голосования: ${txHash}`);
|
||||||
|
|
||||||
|
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 receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут
|
||||||
|
|
||||||
|
if (receipt && receipt.status === 1) {
|
||||||
|
console.log(`[DLE Proposals] Транзакция голосования подтверждена: ${txHash}`);
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление
|
||||||
|
const wsHub = require('../wsHub');
|
||||||
|
wsHub.broadcastProposalVoted(dleAddress, proposalId, support, txHash);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
txHash: txHash,
|
||||||
|
status: 'confirmed',
|
||||||
|
receipt: receipt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Транзакция не подтверждена или провалилась'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Proposals] Ошибка при отслеживании транзакции:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при отслеживании транзакции: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Endpoint для отслеживания подтверждения транзакций исполнения
|
||||||
|
router.post('/track-execution-transaction', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { txHash, dleAddress, proposalId } = req.body;
|
||||||
|
|
||||||
|
if (!txHash || !dleAddress || proposalId === undefined) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Необходимы txHash, dleAddress и proposalId'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Отслеживание транзакции исполнения: ${txHash}`);
|
||||||
|
|
||||||
|
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 receipt = await provider.waitForTransaction(txHash, 1, 60000); // 60 секунд таймаут
|
||||||
|
|
||||||
|
if (receipt && receipt.status === 1) {
|
||||||
|
console.log(`[DLE Proposals] Транзакция исполнения подтверждена: ${txHash}`);
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомление
|
||||||
|
const wsHub = require('../wsHub');
|
||||||
|
wsHub.broadcastProposalExecuted(dleAddress, proposalId, txHash);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
txHash: txHash,
|
||||||
|
status: 'confirmed',
|
||||||
|
receipt: receipt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Транзакция не подтверждена или провалилась'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Proposals] Ошибка при отслеживании транзакции исполнения:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при отслеживании транзакции исполнения: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Декодировать данные предложения о добавлении модуля
|
||||||
|
router.post('/decode-proposal-data', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { transactionHash } = req.body;
|
||||||
|
|
||||||
|
if (!transactionHash) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Хеш транзакции обязателен'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Декодирование данных транзакции: ${transactionHash}`);
|
||||||
|
|
||||||
|
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 tx = await provider.getTransaction(transactionHash);
|
||||||
|
if (!tx) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Транзакция не найдена'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Декодируем данные транзакции
|
||||||
|
const iface = new ethers.Interface([
|
||||||
|
"function createAddModuleProposal(string memory _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) external returns (uint256)"
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = iface.parseTransaction({ data: tx.data });
|
||||||
|
|
||||||
|
const proposalData = {
|
||||||
|
description: decoded.args._description,
|
||||||
|
duration: Number(decoded.args._duration),
|
||||||
|
moduleId: decoded.args._moduleId,
|
||||||
|
moduleAddress: decoded.args._moduleAddress,
|
||||||
|
chainId: Number(decoded.args._chainId)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Декодированные данные:`, proposalData);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: proposalData
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (decodeError) {
|
||||||
|
console.log(`[DLE Proposals] Ошибка декодирования:`, decodeError.message);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Не удалось декодировать данные транзакции: ' + decodeError.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Proposals] Ошибка при декодировании данных предложения:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при декодировании данных предложения: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -18,10 +18,36 @@ const auth = require('../middleware/auth');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const ethers = require('ethers'); // Added ethers for private key validation
|
const ethers = require('ethers'); // Added ethers for private key validation
|
||||||
|
const deploymentTracker = require('../utils/deploymentTracker');
|
||||||
const create2 = require('../utils/create2');
|
const create2 = require('../utils/create2');
|
||||||
const verificationStore = require('../services/verificationStore');
|
const verificationStore = require('../services/verificationStore');
|
||||||
const etherscanV2 = require('../services/etherscanV2VerificationService');
|
const etherscanV2 = require('../services/etherscanV2VerificationService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Асинхронная функция для выполнения деплоя в фоне
|
||||||
|
*/
|
||||||
|
async function executeDeploymentInBackground(deploymentId, dleParams) {
|
||||||
|
try {
|
||||||
|
// Отправляем уведомление о начале
|
||||||
|
deploymentTracker.updateDeployment(deploymentId, {
|
||||||
|
status: 'in_progress',
|
||||||
|
stage: 'initializing'
|
||||||
|
});
|
||||||
|
|
||||||
|
deploymentTracker.addLog(deploymentId, '🚀 Начинаем деплой DLE контракта и модулей', 'info');
|
||||||
|
|
||||||
|
// Выполняем деплой с передачей deploymentId для WebSocket обновлений
|
||||||
|
const result = await dleV2Service.createDLE(dleParams, deploymentId);
|
||||||
|
|
||||||
|
// Завершаем успешно
|
||||||
|
deploymentTracker.completeDeployment(deploymentId, result.data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Завершаем с ошибкой
|
||||||
|
deploymentTracker.failDeployment(deploymentId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route POST /api/dle-v2
|
* @route POST /api/dle-v2
|
||||||
* @desc Создать новое DLE v2 (Digital Legal Entity)
|
* @desc Создать новое DLE v2 (Digital Legal Entity)
|
||||||
@@ -30,7 +56,7 @@ const etherscanV2 = require('../services/etherscanV2VerificationService');
|
|||||||
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const dleParams = req.body;
|
const dleParams = req.body;
|
||||||
logger.info('Получен запрос на создание DLE v2:', dleParams);
|
logger.info('🔥 Получен запрос на асинхронный деплой DLE v2');
|
||||||
|
|
||||||
// Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя
|
// Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя
|
||||||
if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) {
|
if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) {
|
||||||
@@ -51,22 +77,26 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем DLE v2
|
// Создаем запись о деплое
|
||||||
const result = await dleV2Service.createDLE(dleParams);
|
const deploymentId = deploymentTracker.createDeployment(dleParams);
|
||||||
|
|
||||||
logger.info('DLE v2 успешно создано:', result);
|
// Запускаем деплой в фоне (без await!)
|
||||||
|
executeDeploymentInBackground(deploymentId, dleParams);
|
||||||
|
|
||||||
|
logger.info(`📤 Деплой запущен асинхронно: ${deploymentId}`);
|
||||||
|
|
||||||
|
// Сразу возвращаем ответ с ID деплоя
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: 'DLE v2 успешно создано',
|
message: 'Деплой запущен в фоновом режиме',
|
||||||
data: result.data
|
deploymentId: deploymentId
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при создании DLE v2:', error);
|
logger.error('❌ Ошибка при запуске асинхронного деплоя:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || 'Произошла ошибка при создании DLE v2'
|
message: error.message || 'Произошла ошибка при запуске деплоя'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -94,46 +124,6 @@ router.get('/', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @route POST /api/dle-v2/manual-card
|
|
||||||
* @desc Ручное сохранение карточки DLE по адресу (если деплой уже был)
|
|
||||||
* @access Private (admin)
|
|
||||||
*/
|
|
||||||
router.post('/manual-card', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { dleAddress, name, symbol, location, coordinates, jurisdiction, oktmo, okvedCodes, kpp, quorumPercentage, initialPartners, initialAmounts, supportedChainIds, networks } = req.body || {};
|
|
||||||
if (!dleAddress) {
|
|
||||||
return res.status(400).json({ success: false, message: 'dleAddress обязателен' });
|
|
||||||
}
|
|
||||||
const data = {
|
|
||||||
name: name || '',
|
|
||||||
symbol: symbol || '',
|
|
||||||
location: location || '',
|
|
||||||
coordinates: coordinates || '',
|
|
||||||
jurisdiction: jurisdiction ?? 1,
|
|
||||||
oktmo: oktmo ?? null,
|
|
||||||
okvedCodes: Array.isArray(okvedCodes) ? okvedCodes : [],
|
|
||||||
kpp: kpp ?? null,
|
|
||||||
quorumPercentage: quorumPercentage ?? 51,
|
|
||||||
initialPartners: Array.isArray(initialPartners) ? initialPartners : [],
|
|
||||||
initialAmounts: Array.isArray(initialAmounts) ? initialAmounts : [],
|
|
||||||
governanceSettings: {
|
|
||||||
quorumPercentage: quorumPercentage ?? 51,
|
|
||||||
supportedChainIds: Array.isArray(supportedChainIds) ? supportedChainIds : [],
|
|
||||||
currentChainId: Array.isArray(supportedChainIds) && supportedChainIds.length ? supportedChainIds[0] : 1
|
|
||||||
},
|
|
||||||
dleAddress,
|
|
||||||
version: 'v2',
|
|
||||||
networks: Array.isArray(networks) ? networks : [],
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
const savedPath = dleV2Service.saveDLEData(data);
|
|
||||||
return res.json({ success: true, data: { file: savedPath } });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('manual-card error', e);
|
|
||||||
return res.status(500).json({ success: false, message: e.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @route GET /api/dle-v2/default-params
|
* @route GET /api/dle-v2/default-params
|
||||||
@@ -342,35 +332,130 @@ router.post('/validate-private-key', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/dle-v2/deployment-status/:deploymentId
|
||||||
|
* @desc Получить статус деплоя
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.get('/deployment-status/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deploymentId } = req.params;
|
||||||
|
|
||||||
|
const deployment = deploymentTracker.getDeployment(deploymentId);
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Деплой не найден'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: deployment.id,
|
||||||
|
status: deployment.status,
|
||||||
|
stage: deployment.stage,
|
||||||
|
progress: deployment.progress,
|
||||||
|
networks: deployment.networks,
|
||||||
|
startedAt: deployment.startedAt,
|
||||||
|
updatedAt: deployment.updatedAt,
|
||||||
|
logs: deployment.logs.slice(-50), // Последние 50 логов
|
||||||
|
error: deployment.error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении статуса деплоя:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при получении статуса'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/dle-v2/deployment-result/:deploymentId
|
||||||
|
* @desc Получить результат завершенного деплоя
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.get('/deployment-result/:deploymentId', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { deploymentId } = req.params;
|
||||||
|
|
||||||
|
const deployment = deploymentTracker.getDeployment(deploymentId);
|
||||||
|
|
||||||
|
if (!deployment) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Деплой не найден'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deployment.status !== 'completed') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: `Деплой не завершен. Текущий статус: ${deployment.status}`,
|
||||||
|
status: deployment.status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
result: deployment.result,
|
||||||
|
completedAt: deployment.completedAt,
|
||||||
|
duration: deployment.completedAt ? deployment.completedAt - deployment.startedAt : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении результата деплоя:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при получении результата'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/dle-v2/deployment-stats
|
||||||
|
* @desc Получить статистику деплоев
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.get('/deployment-stats', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stats = deploymentTracker.getStats();
|
||||||
|
const activeDeployments = deploymentTracker.getActiveDeployments();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
stats,
|
||||||
|
activeDeployments: activeDeployments.map(d => ({
|
||||||
|
id: d.id,
|
||||||
|
stage: d.stage,
|
||||||
|
progress: d.progress,
|
||||||
|
startedAt: d.startedAt
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении статистики деплоев:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при получении статистики'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Дополнительные маршруты (подключаются из app.js)
|
* Дополнительные маршруты (подключаются из app.js)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Предсказание адресов по выбранным сетям с использованием CREATE2
|
|
||||||
router.post('/predict-addresses', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, symbol, selectedNetworks } = req.body || {};
|
|
||||||
if (!selectedNetworks || !Array.isArray(selectedNetworks) || selectedNetworks.length === 0) {
|
|
||||||
return res.status(400).json({ success: false, message: 'Не переданы сети' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Используем служебные секреты для фабрики и SALT
|
|
||||||
// Factory больше не используется - адреса DLE теперь вычисляются через CREATE с выровненным nonce
|
|
||||||
const result = {};
|
|
||||||
for (const chainId of selectedNetworks) {
|
|
||||||
// Адрес DLE будет одинаковым во всех сетях благодаря выравниванию nonce
|
|
||||||
// Вычисляется в deploy-multichain.js во время деплоя
|
|
||||||
result[chainId] = 'Вычисляется во время деплоя';
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({ success: true, data: result });
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('predict-addresses error', e);
|
|
||||||
return res.status(500).json({ success: false, message: 'Ошибка расчета адресов' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Сохранить GUID верификации (если нужно отдельным вызовом)
|
// Сохранить GUID верификации (если нужно отдельным вызовом)
|
||||||
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ENS utilities: resolve avatar URL for a given ENS name
|
* ENS utilities: resolve avatar URL for a given ENS name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,6 +17,32 @@ const logger = require('../utils/logger');
|
|||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const rpcProviderService = require('../services/rpcProviderService');
|
const rpcProviderService = require('../services/rpcProviderService');
|
||||||
|
|
||||||
|
// Функция для получения информации о сети по chain_id
|
||||||
|
function getNetworkInfo(chainId) {
|
||||||
|
const networkInfo = {
|
||||||
|
1: { name: 'Ethereum Mainnet', description: 'Максимальная безопасность и децентрализация' },
|
||||||
|
137: { name: 'Polygon', description: 'Низкие комиссии, быстрые транзакции' },
|
||||||
|
42161: { name: 'Arbitrum One', description: 'Оптимистичные rollups, средние комиссии' },
|
||||||
|
10: { name: 'Optimism', description: 'Оптимистичные rollups, низкие комиссии' },
|
||||||
|
56: { name: 'BSC', description: 'Совместимость с экосистемой Binance' },
|
||||||
|
43114: { name: 'Avalanche', description: 'Высокая пропускная способность' },
|
||||||
|
11155111: { name: 'Sepolia Testnet', description: 'Тестовая сеть Ethereum' },
|
||||||
|
80001: { name: 'Mumbai Testnet', description: 'Тестовая сеть Polygon' },
|
||||||
|
421613: { name: 'Arbitrum Goerli', description: 'Тестовая сеть Arbitrum' },
|
||||||
|
420: { name: 'Optimism Goerli', description: 'Тестовая сеть Optimism' },
|
||||||
|
97: { name: 'BSC Testnet', description: 'Тестовая сеть BSC' },
|
||||||
|
17000: { name: 'Holesky Testnet', description: 'Тестовая сеть Holesky' },
|
||||||
|
421614: { name: 'Arbitrum Sepolia', description: 'Тестовая сеть Arbitrum Sepolia' },
|
||||||
|
84532: { name: 'Base Sepolia', description: 'Тестовая сеть Base Sepolia' },
|
||||||
|
80002: { name: 'Polygon Amoy', description: 'Тестовая сеть Polygon Amoy' }
|
||||||
|
};
|
||||||
|
|
||||||
|
return networkInfo[chainId] || {
|
||||||
|
name: `Chain ${chainId}`,
|
||||||
|
description: 'Блокчейн сеть'
|
||||||
|
};
|
||||||
|
}
|
||||||
const authTokenService = require('../services/authTokenService');
|
const authTokenService = require('../services/authTokenService');
|
||||||
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
|
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
|
||||||
const aiAssistant = require('../services/ai-assistant');
|
const aiAssistant = require('../services/ai-assistant');
|
||||||
@@ -65,7 +91,15 @@ router.get('/rpc', async (req, res, next) => {
|
|||||||
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
|
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
|
||||||
[encryptionKey]
|
[encryptionKey]
|
||||||
);
|
);
|
||||||
const rpcConfigs = rpcProvidersResult.rows;
|
const rpcConfigs = rpcProvidersResult.rows.map(config => {
|
||||||
|
// Добавляем name и description на основе chain_id
|
||||||
|
const networkInfo = getNetworkInfo(config.chain_id);
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
name: networkInfo.name,
|
||||||
|
description: networkInfo.description
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
// Для админов возвращаем полные данные
|
// Для админов возвращаем полные данные
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Загрузка файлов (логотипы) через Multer
|
* Загрузка файлов (логотипы) через Multer
|
||||||
*/
|
*/
|
||||||
|
|||||||
67
backend/scripts/check-modules.js
Normal file
67
backend/scripts/check-modules.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
|
||||||
|
async function checkModules() {
|
||||||
|
try {
|
||||||
|
// Адрес DLE контракта
|
||||||
|
const dleAddress = '0xCaa85e96a6929F0373442e31FD9888d985869EcE';
|
||||||
|
|
||||||
|
// RPC URL для Sepolia
|
||||||
|
const rpcUrl = 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52';
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// ABI для DLE контракта
|
||||||
|
const dleAbi = [
|
||||||
|
"function modulesInitialized() external view returns (bool)",
|
||||||
|
"function initializer() external view returns (address)",
|
||||||
|
"function isModuleActive(bytes32 _moduleId) external view returns (bool)",
|
||||||
|
"function getModuleAddress(bytes32 _moduleId) external view returns (address)",
|
||||||
|
"function initializeBaseModules(address _treasuryAddress, address _timelockAddress, address _readerAddress) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Проверяем статус инициализации
|
||||||
|
const modulesInitialized = await dle.modulesInitialized();
|
||||||
|
console.log('Модули инициализированы:', modulesInitialized);
|
||||||
|
|
||||||
|
// Получаем initializer адрес
|
||||||
|
const initializer = await dle.initializer();
|
||||||
|
console.log('Initializer адрес:', initializer);
|
||||||
|
|
||||||
|
// Проверяем модули
|
||||||
|
const moduleIds = {
|
||||||
|
treasury: ethers.keccak256(ethers.toUtf8Bytes("TREASURY")),
|
||||||
|
timelock: ethers.keccak256(ethers.toUtf8Bytes("TIMELOCK")),
|
||||||
|
reader: ethers.keccak256(ethers.toUtf8Bytes("READER"))
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('\nПроверка модулей:');
|
||||||
|
for (const [name, moduleId] of Object.entries(moduleIds)) {
|
||||||
|
try {
|
||||||
|
const isActive = await dle.isModuleActive(moduleId);
|
||||||
|
const address = await dle.getModuleAddress(moduleId);
|
||||||
|
console.log(`${name}: активен=${isActive}, адрес=${address}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${name}: ошибка - ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkModules();
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"moduleType": "reader",
|
||||||
|
"dleAddress": "0x4e2A2B5FcA4edaBb537710D9682C40C3dc3e8dE2",
|
||||||
|
"networks": [
|
||||||
|
{
|
||||||
|
"chainId": 11155111,
|
||||||
|
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 17000,
|
||||||
|
"rpcUrl": "https://ethereum-holesky.publicnode.com",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 421614,
|
||||||
|
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 84532,
|
||||||
|
"rpcUrl": "https://sepolia.base.org",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deployTimestamp": "2025-09-22T23:19:13.695Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"moduleType": "timelock",
|
||||||
|
"dleAddress": "0x4e2A2B5FcA4edaBb537710D9682C40C3dc3e8dE2",
|
||||||
|
"networks": [
|
||||||
|
{
|
||||||
|
"chainId": 11155111,
|
||||||
|
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 17000,
|
||||||
|
"rpcUrl": "https://ethereum-holesky.publicnode.com",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 421614,
|
||||||
|
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 84532,
|
||||||
|
"rpcUrl": "https://sepolia.base.org",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deployTimestamp": "2025-09-22T23:19:13.054Z"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"moduleType": "treasury",
|
||||||
|
"dleAddress": "0x4e2A2B5FcA4edaBb537710D9682C40C3dc3e8dE2",
|
||||||
|
"networks": [
|
||||||
|
{
|
||||||
|
"chainId": 11155111,
|
||||||
|
"rpcUrl": "https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 17000,
|
||||||
|
"rpcUrl": "https://ethereum-holesky.publicnode.com",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 421614,
|
||||||
|
"rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chainId": 84532,
|
||||||
|
"rpcUrl": "https://sepolia.base.org",
|
||||||
|
"address": null,
|
||||||
|
"verification": "unknown"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deployTimestamp": "2025-09-22T23:19:11.085Z"
|
||||||
|
}
|
||||||
205
backend/scripts/deploy/deploy-multichain.js
Normal file → Executable file
205
backend/scripts/deploy/deploy-multichain.js
Normal file → Executable file
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
const hre = require('hardhat');
|
const hre = require('hardhat');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
@@ -48,6 +60,12 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
|
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current < targetDLENonce) {
|
||||||
|
console.log(`[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}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (current < targetDLENonce) {
|
if (current < targetDLENonce) {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
||||||
|
|
||||||
@@ -70,9 +88,11 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
...overrides
|
...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)} 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);
|
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();
|
await txFill.wait();
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`);
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
|
||||||
sent = true;
|
sent = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
lastErr = e;
|
lastErr = e;
|
||||||
@@ -103,6 +123,7 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${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}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`);
|
||||||
}
|
}
|
||||||
@@ -257,12 +278,22 @@ async function deployModulesInNetwork(rpcUrl, pk, dleAddress, params) {
|
|||||||
const readerAddress = modules.dleReader;
|
const readerAddress = modules.dleReader;
|
||||||
|
|
||||||
if (treasuryAddress && timelockAddress && readerAddress) {
|
if (treasuryAddress && timelockAddress && readerAddress) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} All modules deployed, initializing...`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Treasury: ${treasuryAddress}`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Timelock: ${timelockAddress}`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Reader: ${readerAddress}`);
|
||||||
|
|
||||||
// Инициализация базовых модулей
|
// Инициализация базовых модулей
|
||||||
await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
|
const initTx = await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`);
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Module initialization tx: ${initTx.hash}`);
|
||||||
|
await initTx.wait();
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized successfully`);
|
||||||
currentNonce++;
|
currentNonce++;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`);
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Treasury: ${treasuryAddress || 'MISSING'}`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Timelock: ${timelockAddress || 'MISSING'}`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} Reader: ${readerAddress || 'MISSING'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message);
|
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message);
|
||||||
@@ -516,34 +547,70 @@ async function main() {
|
|||||||
}
|
}
|
||||||
const targetDLENonce = Math.max(...nonces);
|
const targetDLENonce = Math.max(...nonces);
|
||||||
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
|
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
|
||||||
|
console.log(`[MULTI_DBG] Starting deployment to ${networks.length} networks:`, networks);
|
||||||
|
|
||||||
const results = [];
|
// ПАРАЛЛЕЛЬНЫЙ деплой во всех сетях одновременно
|
||||||
for (let i = 0; i < networks.length; i++) {
|
console.log(`[MULTI_DBG] Starting PARALLEL deployment to ${networks.length} networks`);
|
||||||
const rpcUrl = networks[i];
|
|
||||||
console.log(`[MULTI_DBG] deploying to network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
const deploymentPromises = networks.map(async (rpcUrl, i) => {
|
||||||
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit);
|
console.log(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
||||||
results.push({ rpcUrl, ...r });
|
|
||||||
}
|
try {
|
||||||
|
// Получаем chainId динамически из сети
|
||||||
|
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = Number(network.chainId);
|
||||||
|
|
||||||
|
console.log(`[MULTI_DBG] 📡 Network ${i + 1} chainId: ${chainId}`);
|
||||||
|
|
||||||
|
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit);
|
||||||
|
console.log(`[MULTI_DBG] ✅ Network ${i + 1} (chainId: ${chainId}) deployment SUCCESS: ${r.address}`);
|
||||||
|
return { rpcUrl, chainId, ...r };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MULTI_DBG] ❌ Network ${i + 1} deployment FAILED:`, error.message);
|
||||||
|
return { rpcUrl, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ждем завершения всех деплоев
|
||||||
|
const results = await Promise.all(deploymentPromises);
|
||||||
|
console.log(`[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}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[MULTI_DBG] ❌ Network ${index + 1} (chainId: ${result.chainId}) FAILED: ${result.error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Проверяем, что все адреса одинаковые
|
// Проверяем, что все адреса одинаковые
|
||||||
const addresses = results.map(r => r.address);
|
const addresses = results.map(r => r.address).filter(addr => addr);
|
||||||
const uniqueAddresses = [...new Set(addresses)];
|
const uniqueAddresses = [...new Set(addresses)];
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] All addresses:', addresses);
|
||||||
|
console.log('[MULTI_DBG] Unique addresses:', uniqueAddresses);
|
||||||
|
console.log('[MULTI_DBG] Results count:', results.length);
|
||||||
|
console.log('[MULTI_DBG] Networks count:', networks.length);
|
||||||
|
|
||||||
if (uniqueAddresses.length > 1) {
|
if (uniqueAddresses.length > 1) {
|
||||||
console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
|
console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
|
||||||
console.error('[MULTI_DBG] addresses:', uniqueAddresses);
|
console.error('[MULTI_DBG] addresses:', uniqueAddresses);
|
||||||
throw new Error('Nonce alignment failed - addresses are different');
|
throw new Error('Nonce alignment failed - addresses are different');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uniqueAddresses.length === 0) {
|
||||||
|
console.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]);
|
console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
|
||||||
|
|
||||||
// Деплой модулей во всех сетях
|
// Деплой модулей ОТКЛЮЧЕН - модули будут деплоиться отдельно
|
||||||
console.log('[MULTI_DBG] Starting module deployment...');
|
console.log('[MULTI_DBG] Module deployment DISABLED - modules will be deployed separately');
|
||||||
const moduleResults = await deployModulesInAllNetworks(networks, pk, uniqueAddresses[0], params);
|
const moduleResults = [];
|
||||||
|
const verificationResults = [];
|
||||||
// Верификация контрактов
|
|
||||||
console.log('[MULTI_DBG] Starting contract verification...');
|
|
||||||
const verificationResults = await verifyContractsInAllNetworks(networks, pk, uniqueAddresses[0], moduleResults, params);
|
|
||||||
|
|
||||||
// Объединяем результаты
|
// Объединяем результаты
|
||||||
const finalResults = results.map((result, index) => ({
|
const finalResults = results.map((result, index) => ({
|
||||||
@@ -554,62 +621,62 @@ async function main() {
|
|||||||
|
|
||||||
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
|
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
|
||||||
|
|
||||||
// Сохраняем информацию о модулях в отдельный файл для каждого DLE
|
// Сохраняем каждый модуль в отдельный файл
|
||||||
// Добавляем информацию о сетях (chainId, rpcUrl)
|
const dleAddress = uniqueAddresses[0];
|
||||||
const modulesInfo = {
|
const modulesDir = path.join(__dirname, '../contracts-data/modules');
|
||||||
dleAddress: uniqueAddresses[0],
|
if (!fs.existsSync(modulesDir)) {
|
||||||
networks: networks.map((rpcUrl, index) => ({
|
fs.mkdirSync(modulesDir, { recursive: true });
|
||||||
rpcUrl: rpcUrl,
|
}
|
||||||
chainId: null, // Будет заполнено ниже
|
|
||||||
networkName: null // Будет заполнено ниже
|
|
||||||
})),
|
|
||||||
modules: moduleResults,
|
|
||||||
verification: verificationResults,
|
|
||||||
deployTimestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Получаем chainId для каждой сети
|
// Создаем файлы для каждого типа модуля
|
||||||
for (let i = 0; i < networks.length; i++) {
|
const moduleTypes = ['treasury', 'timelock', 'reader'];
|
||||||
try {
|
const moduleKeys = ['treasuryModule', 'timelockModule', 'dleReader'];
|
||||||
const provider = new hre.ethers.JsonRpcProvider(networks[i]);
|
|
||||||
const network = await provider.getNetwork();
|
for (let moduleIndex = 0; moduleIndex < moduleTypes.length; moduleIndex++) {
|
||||||
modulesInfo.networks[i].chainId = Number(network.chainId);
|
const moduleType = moduleTypes[moduleIndex];
|
||||||
|
const moduleKey = moduleKeys[moduleIndex];
|
||||||
|
|
||||||
|
const moduleInfo = {
|
||||||
|
moduleType: moduleType,
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
networks: [],
|
||||||
|
deployTimestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Собираем адреса модуля во всех сетях
|
||||||
|
for (let i = 0; i < networks.length; i++) {
|
||||||
|
const rpcUrl = networks[i];
|
||||||
|
const moduleResult = moduleResults[i];
|
||||||
|
|
||||||
// Определяем название сети по chainId
|
try {
|
||||||
const networkNames = {
|
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
|
||||||
1: 'Ethereum Mainnet',
|
const network = await provider.getNetwork();
|
||||||
5: 'Goerli',
|
|
||||||
11155111: 'Sepolia',
|
moduleInfo.networks.push({
|
||||||
137: 'Polygon Mainnet',
|
chainId: Number(network.chainId),
|
||||||
80001: 'Mumbai',
|
rpcUrl: rpcUrl,
|
||||||
56: 'BSC Mainnet',
|
address: moduleResult && moduleResult[moduleKey] ? moduleResult[moduleKey] : null,
|
||||||
97: 'BSC Testnet',
|
verification: verificationResults[i] && verificationResults[i][moduleKey] ? verificationResults[i][moduleKey] : 'unknown'
|
||||||
42161: 'Arbitrum One',
|
});
|
||||||
421614: 'Arbitrum Sepolia',
|
} catch (error) {
|
||||||
10: 'Optimism',
|
console.error(`[MULTI_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message);
|
||||||
11155420: 'Optimism Sepolia',
|
moduleInfo.networks.push({
|
||||||
8453: 'Base',
|
chainId: null,
|
||||||
84532: 'Base Sepolia'
|
rpcUrl: rpcUrl,
|
||||||
};
|
address: null,
|
||||||
modulesInfo.networks[i].networkName = networkNames[Number(network.chainId)] || `Chain ID ${Number(network.chainId)}`;
|
verification: 'error'
|
||||||
|
});
|
||||||
console.log(`[MULTI_DBG] Сеть ${i + 1}: chainId=${Number(network.chainId)}, name=${modulesInfo.networks[i].networkName}`);
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(`[MULTI_DBG] Ошибка получения chainId для сети ${i + 1}:`, error.message);
|
|
||||||
modulesInfo.networks[i].chainId = null;
|
|
||||||
modulesInfo.networks[i].networkName = `Сеть ${i + 1}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сохраняем файл модуля
|
||||||
|
const moduleFileName = `${moduleType}-${dleAddress.toLowerCase()}.json`;
|
||||||
|
const moduleFilePath = path.join(modulesDir, moduleFileName);
|
||||||
|
fs.writeFileSync(moduleFilePath, JSON.stringify(moduleInfo, null, 2));
|
||||||
|
console.log(`[MULTI_DBG] Module ${moduleType} saved to: ${moduleFilePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем директорию temp если её нет
|
console.log(`[MULTI_DBG] All modules saved to separate files in: ${modulesDir}`);
|
||||||
const tempDir = path.join(__dirname, '../temp');
|
|
||||||
if (!fs.existsSync(tempDir)) {
|
|
||||||
fs.mkdirSync(tempDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const deployResultPath = path.join(tempDir, `modules-${uniqueAddresses[0].toLowerCase()}.json`);
|
|
||||||
fs.writeFileSync(deployResultPath, JSON.stringify(modulesInfo, null, 2));
|
|
||||||
console.log(`[MULTI_DBG] Modules info saved to: ${deployResultPath}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e) => { console.error(e); process.exit(1); });
|
main().catch((e) => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
@@ -66,15 +66,22 @@ initWSS(server);
|
|||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initDbPool(); // Дождаться пересоздания пула!
|
await initDbPool(); // Дождаться пересоздания пула!
|
||||||
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
|
|
||||||
|
// Инициализация AI ассистента В ФОНЕ (неблокирующая)
|
||||||
|
seedAIAssistantSettings().catch(error => {
|
||||||
|
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
// Разогрев модели Ollama
|
// Разогрев модели Ollama
|
||||||
// console.log('🔥 Запуск разогрева модели...');
|
// console.log('🔥 Запуск разогрева модели...');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
}, 10000); // Задержка 10 секунд для полной инициализации
|
}, 10000); // Задержка 10 секунд для полной инициализации
|
||||||
|
|
||||||
await initServices(); // Только теперь запускать сервисы
|
// Запускаем сервисы в фоне (неблокирующе)
|
||||||
// console.log(`Server is running on port ${PORT}`);
|
initServices().catch(error => {
|
||||||
|
console.warn('[Server] Ошибка инициализации сервисов:', error.message);
|
||||||
|
});
|
||||||
|
console.log(`✅ Server is running on port ${PORT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(PORT, async () => {
|
server.listen(PORT, async () => {
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сервис для динамического управления подключениями к базе данных
|
* Сервис для динамического управления подключениями к базе данных
|
||||||
* Позволяет изменять настройки БД без перезапуска приложения
|
* Позволяет изменять настройки БД без перезапуска приложения
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const fs = require('fs');
|
|||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
||||||
|
const deploymentTracker = require('../utils/deploymentTracker');
|
||||||
const etherscanV2 = require('./etherscanV2VerificationService');
|
const etherscanV2 = require('./etherscanV2VerificationService');
|
||||||
const verificationStore = require('./verificationStore');
|
const verificationStore = require('./verificationStore');
|
||||||
|
|
||||||
@@ -29,11 +30,18 @@ class DLEV2Service {
|
|||||||
* @param {Object} dleParams - Параметры DLE
|
* @param {Object} dleParams - Параметры DLE
|
||||||
* @returns {Promise<Object>} - Результат создания DLE
|
* @returns {Promise<Object>} - Результат создания DLE
|
||||||
*/
|
*/
|
||||||
async createDLE(dleParams) {
|
async createDLE(dleParams, deploymentId = null) {
|
||||||
|
console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!");
|
||||||
|
logger.info("🚀 DEBUG: ВХОДИМ В createDLE ФУНКЦИЮ");
|
||||||
let paramsFile = null;
|
let paramsFile = null;
|
||||||
let tempParamsFile = null;
|
let tempParamsFile = null;
|
||||||
try {
|
try {
|
||||||
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
|
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
|
||||||
|
|
||||||
|
// WebSocket обновление: начало процесса
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Валидация параметров', 5, 'Проверяем входные данные');
|
||||||
|
}
|
||||||
|
|
||||||
// Валидация входных данных
|
// Валидация входных данных
|
||||||
this.validateDLEParams(dleParams);
|
this.validateDLEParams(dleParams);
|
||||||
@@ -50,6 +58,11 @@ class DLEV2Service {
|
|||||||
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
|
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSocket обновление: генерация CREATE2_SALT
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Генерация CREATE2 SALT', 10, 'Создаем уникальный идентификатор для детерминированного адреса');
|
||||||
|
}
|
||||||
|
|
||||||
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
|
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
|
||||||
const { createAndStoreNewCreate2Salt } = require('./secretStore');
|
const { createAndStoreNewCreate2Salt } = require('./secretStore');
|
||||||
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
|
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
|
||||||
@@ -66,6 +79,11 @@ class DLEV2Service {
|
|||||||
}
|
}
|
||||||
fs.copyFileSync(paramsFile, tempParamsFile);
|
fs.copyFileSync(paramsFile, tempParamsFile);
|
||||||
|
|
||||||
|
// WebSocket обновление: поиск RPC URLs
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Поиск RPC endpoints', 15, 'Подключаемся к блокчейн сетям');
|
||||||
|
}
|
||||||
|
|
||||||
// Готовим RPC для всех выбранных сетей
|
// Готовим RPC для всех выбранных сетей
|
||||||
const rpcUrls = [];
|
const rpcUrls = [];
|
||||||
for (const cid of deployParams.supportedChainIds) {
|
for (const cid of deployParams.supportedChainIds) {
|
||||||
@@ -99,14 +117,7 @@ class DLEV2Service {
|
|||||||
const walletAddress = new ethers.Wallet(pk, provider).address;
|
const walletAddress = new ethers.Wallet(pk, provider).address;
|
||||||
const balance = await provider.getBalance(walletAddress);
|
const balance = await provider.getBalance(walletAddress);
|
||||||
|
|
||||||
if (typeof ethers.parseEther !== 'function') {
|
|
||||||
throw new Error('Метод ethers.parseEther не найден');
|
|
||||||
}
|
|
||||||
const minBalance = ethers.parseEther("0.00001");
|
const minBalance = ethers.parseEther("0.00001");
|
||||||
|
|
||||||
if (typeof ethers.formatEther !== 'function') {
|
|
||||||
throw new Error('Метод ethers.formatEther не найден');
|
|
||||||
}
|
|
||||||
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
|
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
|
||||||
if (balance < minBalance) {
|
if (balance < minBalance) {
|
||||||
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
|
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
|
||||||
@@ -117,27 +128,87 @@ class DLEV2Service {
|
|||||||
throw new Error('Приватный ключ для деплоя не передан');
|
throw new Error('Приватный ключ для деплоя не передан');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Рассчитываем INIT_CODE_HASH автоматически из актуального initCode
|
// Сохраняем ключ Etherscan V2 ПЕРЕД деплоем
|
||||||
const initCodeHash = await this.computeInitCodeHash({
|
logger.info(`🔑 Etherscan API Key получен: ${dleParams.etherscanApiKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
|
||||||
...deployParams,
|
try {
|
||||||
currentChainId: deployParams.currentChainId || deployParams.supportedChainIds[0]
|
if (dleParams.etherscanApiKey) {
|
||||||
});
|
logger.info('🔑 Сохраняем Etherscan API Key в secretStore...');
|
||||||
|
const { setSecret } = require('./secretStore');
|
||||||
|
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
|
||||||
|
logger.info('🔑 Etherscan API Key успешно сохранен в базу данных');
|
||||||
|
} else {
|
||||||
|
logger.warn('🔑 Etherscan API Key не передан, пропускаем сохранение');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔑 Ошибка при сохранении Etherscan API Key:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket обновление: компиляция произойдет автоматически в deploy-multichain.js
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Подготовка к деплою', 25, 'Подготавливаем параметры для деплоя');
|
||||||
|
}
|
||||||
|
|
||||||
|
// INIT_CODE_HASH будет вычислен в deploy-multichain.js
|
||||||
|
|
||||||
// Factory больше не используется - деплой DLE напрямую
|
// Factory больше не используется - деплой DLE напрямую
|
||||||
logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`);
|
logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`);
|
||||||
|
|
||||||
|
// WebSocket обновление: начало мульти-чейн деплоя
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Мульти-чейн деплой', 40);
|
||||||
|
deploymentTracker.addLog(deploymentId, `🌐 Деплой в ${deployParams.supportedChainIds.length} сетях: ${deployParams.supportedChainIds.join(', ')}`, 'info');
|
||||||
|
deploymentTracker.addLog(deploymentId, `📋 Этапы: 1) DLE контракт → 2) Модули → 3) Инициализация → 4) Верификация`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
// Мультисетевой деплой одним вызовом
|
// Мультисетевой деплой одним вызовом
|
||||||
logger.info('Запуск мульти-чейн деплоя...');
|
logger.info('Запуск мульти-чейн деплоя...');
|
||||||
|
logger.info("🔍 DEBUG: Подготовка к прямому деплою...");
|
||||||
|
|
||||||
const result = await this.runDeployMultichain(paramsFile, {
|
const result = await this.runDeployMultichain(paramsFile, {
|
||||||
rpcUrls: rpcUrls,
|
rpcUrls: rpcUrls,
|
||||||
chainIds: deployParams.supportedChainIds,
|
chainIds: deployParams.supportedChainIds,
|
||||||
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
|
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
|
||||||
salt: create2Salt,
|
salt: create2Salt,
|
||||||
initCodeHash
|
etherscanApiKey: dleParams.etherscanApiKey
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2));
|
logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2));
|
||||||
|
logger.info("🔍 DEBUG: Запуск мультисетевого деплоя...");
|
||||||
|
|
||||||
|
// WebSocket обновление: деплой завершен, начинаем обработку результатов
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Обработка результатов', 85, 'Деплой завершен, сохраняем результаты');
|
||||||
|
deploymentTracker.addLog(deploymentId, `✅ DLE контракт задеплоен в ${result.networks?.length || 0} сетях`, 'success');
|
||||||
|
if (result.networks) {
|
||||||
|
result.networks.forEach(network => {
|
||||||
|
deploymentTracker.addLog(deploymentId, `📍 ${network.networkName || `Chain ${network.chainId}`}: ${network.address}`, 'info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем информацию о модулях
|
||||||
|
if (result.modules) {
|
||||||
|
deploymentTracker.addLog(deploymentId, `🔧 Модули задеплоены в ${result.modules.length} сетях`, 'info');
|
||||||
|
result.modules.forEach((moduleSet, index) => {
|
||||||
|
if (moduleSet && !moduleSet.error) {
|
||||||
|
deploymentTracker.addLog(deploymentId, `📦 Сеть ${index + 1}: Treasury=${moduleSet.treasuryModule?.substring(0, 10)}..., Timelock=${moduleSet.timelockModule?.substring(0, 10)}..., Reader=${moduleSet.dleReader?.substring(0, 10)}...`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем информацию о верификации
|
||||||
|
if (result.verification) {
|
||||||
|
deploymentTracker.addLog(deploymentId, `🔍 Верификация выполнена в ${result.verification.length} сетях`, 'info');
|
||||||
|
result.verification.forEach((verification, index) => {
|
||||||
|
if (verification && !verification.error) {
|
||||||
|
const dleStatus = verification.dle === 'success' ? '✅' : '❌';
|
||||||
|
const treasuryStatus = verification.treasuryModule === 'success' ? '✅' : '❌';
|
||||||
|
const timelockStatus = verification.timelockModule === 'success' ? '✅' : '❌';
|
||||||
|
const readerStatus = verification.dleReader === 'success' ? '✅' : '❌';
|
||||||
|
deploymentTracker.addLog(deploymentId, `🔍 Сеть ${index + 1}: DLE${dleStatus} Treasury${treasuryStatus} Timelock${timelockStatus} Reader${readerStatus}`, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем информацию о созданном DLE для отображения на странице управления
|
// Сохраняем информацию о созданном DLE для отображения на странице управления
|
||||||
try {
|
try {
|
||||||
@@ -148,6 +219,7 @@ class DLEV2Service {
|
|||||||
logger.error('Неверная структура результата деплоя:', result);
|
logger.error('Неверная структура результата деплоя:', result);
|
||||||
throw new Error('Неверная структура результата деплоя');
|
throw new Error('Неверная структура результата деплоя');
|
||||||
}
|
}
|
||||||
|
logger.info("🔍 DEBUG: Вызываем runDeployMultichain...");
|
||||||
|
|
||||||
// Если результат - массив (прямой результат из скрипта), преобразуем его
|
// Если результат - массив (прямой результат из скрипта), преобразуем его
|
||||||
let deployResult = result;
|
let deployResult = result;
|
||||||
@@ -209,6 +281,14 @@ class DLEV2Service {
|
|||||||
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
|
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
|
||||||
// logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование
|
// logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование
|
||||||
|
|
||||||
|
// WebSocket обновление: финализация
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.updateProgress(deploymentId, 'Завершение', 100, 'Деплой успешно завершен!');
|
||||||
|
deploymentTracker.addLog(deploymentId, `🎉 DLE ${result.data.name} (${result.data.symbol}) успешно создан!`, 'success');
|
||||||
|
deploymentTracker.addLog(deploymentId, `📊 Партнеров: ${result.data.partnerBalances?.length || 0}`, 'info');
|
||||||
|
deploymentTracker.addLog(deploymentId, `💰 Общий supply: ${result.data.totalSupply || 'N/A'}`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: dleData
|
data: dleData
|
||||||
@@ -220,31 +300,25 @@ class DLEV2Service {
|
|||||||
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
|
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан
|
// Etherscan API Key уже сохранен в начале функции
|
||||||
try {
|
|
||||||
if (dleParams.etherscanApiKey) {
|
|
||||||
const { setSecret } = require('./secretStore');
|
|
||||||
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
// Авто-верификация через Etherscan V2 (опционально)
|
// Верификация выполняется в deploy-multichain.js
|
||||||
if (dleParams.autoVerifyAfterDeploy) {
|
|
||||||
try {
|
// WebSocket обновление: деплой успешно завершен
|
||||||
await this.autoVerifyAcrossChains({
|
if (deploymentId) {
|
||||||
deployParams,
|
deploymentTracker.completeDeployment(deploymentId, result);
|
||||||
deployResult: result,
|
|
||||||
apiKey: dleParams.etherscanApiKey
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('Авто-верификация завершилась с ошибкой:', e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Ошибка при создании DLE v2:', error);
|
logger.error('Ошибка при создании DLE v2:', error);
|
||||||
|
|
||||||
|
// WebSocket обновление: деплой завершился с ошибкой
|
||||||
|
if (deploymentId) {
|
||||||
|
deploymentTracker.failDeployment(deploymentId, error);
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
@@ -423,9 +497,6 @@ class DLEV2Service {
|
|||||||
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
|
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
|
||||||
try {
|
try {
|
||||||
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
|
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
|
||||||
if (typeof ethers.parseUnits !== 'function') {
|
|
||||||
throw new Error('Метод ethers.parseUnits не найден');
|
|
||||||
}
|
|
||||||
return ethers.parseUnits(rawAmount.toString(), 18).toString();
|
return ethers.parseUnits(rawAmount.toString(), 18).toString();
|
||||||
}
|
}
|
||||||
if (typeof rawAmount === 'string') {
|
if (typeof rawAmount === 'string') {
|
||||||
@@ -435,9 +506,6 @@ class DLEV2Service {
|
|||||||
return BigInt(a).toString();
|
return BigInt(a).toString();
|
||||||
}
|
}
|
||||||
// Десятичная строка — конвертируем в base units
|
// Десятичная строка — конвертируем в base units
|
||||||
if (typeof ethers.parseUnits !== 'function') {
|
|
||||||
throw new Error('Метод ethers.parseUnits не найден');
|
|
||||||
}
|
|
||||||
return ethers.parseUnits(a, 18).toString();
|
return ethers.parseUnits(a, 18).toString();
|
||||||
}
|
}
|
||||||
// BigInt или иные типы — приводим к строке без изменения масштаба
|
// BigInt или иные типы — приводим к строке без изменения масштаба
|
||||||
@@ -530,7 +598,7 @@ class DLEV2Service {
|
|||||||
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
|
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
|
||||||
cwd: path.join(__dirname, '..'),
|
cwd: path.join(__dirname, '..'),
|
||||||
env: envVars,
|
env: envVars,
|
||||||
stdio: 'pipe'
|
stdio: ['inherit', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
@@ -575,13 +643,19 @@ class DLEV2Service {
|
|||||||
|
|
||||||
const envVars = {
|
const envVars = {
|
||||||
...process.env,
|
...process.env,
|
||||||
PRIVATE_KEY: opts.privateKey
|
PRIVATE_KEY: opts.privateKey,
|
||||||
|
ETHERSCAN_API_KEY: opts.etherscanApiKey || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.info(`🔑 Передаем в deploy-multichain.js: ETHERSCAN_API_KEY=${opts.etherscanApiKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
|
||||||
|
logger.info(`🔑 Передаем в deploy-multichain.js: PRIVATE_KEY=${opts.privateKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
|
||||||
|
logger.info(`🔑 PRIVATE_KEY длина: ${opts.privateKey ? opts.privateKey.length : 0}`);
|
||||||
|
logger.info(`🔑 PRIVATE_KEY значение: ${opts.privateKey ? opts.privateKey.substring(0, 10) + '...' : 'undefined'}`);
|
||||||
|
|
||||||
const p = spawn('npx', ['hardhat', 'run', scriptPath], {
|
const p = spawn('npx', ['hardhat', 'run', scriptPath], {
|
||||||
cwd: path.join(__dirname, '..'),
|
cwd: path.join(__dirname, '..'),
|
||||||
env: envVars,
|
env: envVars,
|
||||||
stdio: 'pipe'
|
stdio: ['inherit', 'pipe', 'pipe']
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '', stderr = '';
|
let stdout = '', stderr = '';
|
||||||
@@ -838,11 +912,11 @@ class DLEV2Service {
|
|||||||
|
|
||||||
// Преобразуем группы в массив
|
// Преобразуем группы в массив
|
||||||
return Array.from(groups.values()).map(group => ({
|
return Array.from(groups.values()).map(group => ({
|
||||||
...group,
|
...group,
|
||||||
// Основной адрес DLE (из первой сети)
|
// Основной адрес DLE (из первой сети)
|
||||||
dleAddress: group.networks[0]?.dleAddress,
|
dleAddress: group.networks[0]?.dleAddress,
|
||||||
// Общее количество сетей
|
// Общее количество сетей
|
||||||
totalNetworks: group.networks.length,
|
totalNetworks: group.networks.length,
|
||||||
// Поддерживаемые сети
|
// Поддерживаемые сети
|
||||||
supportedChainIds: group.networks.map(n => n.chainId)
|
supportedChainIds: group.networks.map(n => n.chainId)
|
||||||
}));
|
}));
|
||||||
@@ -894,96 +968,6 @@ class DLEV2Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Авто-расчёт INIT_CODE_HASH
|
|
||||||
async computeInitCodeHash(params) {
|
|
||||||
try {
|
|
||||||
// Проверяем наличие обязательных параметров
|
|
||||||
if (!params.name || !params.symbol || !params.location) {
|
|
||||||
throw new Error('Отсутствуют обязательные параметры для вычисления INIT_CODE_HASH');
|
|
||||||
}
|
|
||||||
|
|
||||||
const hre = require('hardhat');
|
|
||||||
const { ethers } = hre;
|
|
||||||
|
|
||||||
// Проверяем, что контракт DLE существует
|
|
||||||
try {
|
|
||||||
const DLE = await hre.ethers.getContractFactory('DLE');
|
|
||||||
if (!DLE) {
|
|
||||||
throw new Error('Контракт DLE не найден в Hardhat');
|
|
||||||
}
|
|
||||||
} catch (contractError) {
|
|
||||||
throw new Error(`Ошибка загрузки контракта DLE: ${contractError.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DLE = await hre.ethers.getContractFactory('DLE');
|
|
||||||
const dleConfig = {
|
|
||||||
name: params.name,
|
|
||||||
symbol: params.symbol,
|
|
||||||
location: params.location,
|
|
||||||
coordinates: params.coordinates || "",
|
|
||||||
jurisdiction: params.jurisdiction || 1,
|
|
||||||
okvedCodes: params.okvedCodes || [],
|
|
||||||
kpp: params.kpp || 0,
|
|
||||||
quorumPercentage: params.quorumPercentage || 51,
|
|
||||||
initialPartners: params.initialPartners || [],
|
|
||||||
initialAmounts: params.initialAmounts || [],
|
|
||||||
supportedChainIds: params.supportedChainIds || [1]
|
|
||||||
};
|
|
||||||
// Учитываем актуальную сигнатуру конструктора: (dleConfig, currentChainId, initializer)
|
|
||||||
const initializer = params.initializerAddress || "0x0000000000000000000000000000000000000000";
|
|
||||||
const currentChainId = params.currentChainId || 1; // Fallback на Ethereum mainnet
|
|
||||||
|
|
||||||
logger.info('Вычисление INIT_CODE_HASH с параметрами:', {
|
|
||||||
name: dleConfig.name,
|
|
||||||
symbol: dleConfig.symbol,
|
|
||||||
currentChainId,
|
|
||||||
initializer
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем, что метод getDeployTransaction существует
|
|
||||||
if (typeof DLE.getDeployTransaction !== 'function') {
|
|
||||||
throw new Error('Метод getDeployTransaction не найден в контракте DLE');
|
|
||||||
}
|
|
||||||
|
|
||||||
const deployTx = await DLE.getDeployTransaction(dleConfig, currentChainId, initializer);
|
|
||||||
if (!deployTx || !deployTx.data) {
|
|
||||||
throw new Error('Не удалось получить данные транзакции деплоя');
|
|
||||||
}
|
|
||||||
|
|
||||||
const initCode = deployTx.data;
|
|
||||||
|
|
||||||
// Проверяем, что метод keccak256 существует
|
|
||||||
if (typeof ethers.keccak256 !== 'function') {
|
|
||||||
throw new Error('Метод ethers.keccak256 не найден');
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = ethers.keccak256(initCode);
|
|
||||||
|
|
||||||
logger.info('INIT_CODE_HASH вычислен успешно:', hash);
|
|
||||||
return hash;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Ошибка при вычислении INIT_CODE_HASH:', error);
|
|
||||||
// Fallback: возвращаем хеш на основе параметров
|
|
||||||
const { ethers } = require('ethers');
|
|
||||||
const fallbackData = JSON.stringify({
|
|
||||||
name: params.name,
|
|
||||||
symbol: params.symbol,
|
|
||||||
location: params.location,
|
|
||||||
jurisdiction: params.jurisdiction,
|
|
||||||
supportedChainIds: params.supportedChainIds
|
|
||||||
});
|
|
||||||
|
|
||||||
// Проверяем, что методы существуют
|
|
||||||
if (typeof ethers.toUtf8Bytes !== 'function') {
|
|
||||||
throw new Error('Метод ethers.toUtf8Bytes не найден');
|
|
||||||
}
|
|
||||||
if (typeof ethers.keccak256 !== 'function') {
|
|
||||||
throw new Error('Метод ethers.keccak256 не найден');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ethers.keccak256(ethers.toUtf8Bytes(fallbackData));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1017,14 +1001,7 @@ class DLEV2Service {
|
|||||||
const wallet = new ethers.Wallet(privateKey, provider);
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
const balance = await provider.getBalance(wallet.address);
|
const balance = await provider.getBalance(wallet.address);
|
||||||
|
|
||||||
if (typeof ethers.formatEther !== 'function') {
|
|
||||||
throw new Error('Метод ethers.formatEther не найден');
|
|
||||||
}
|
|
||||||
const balanceEth = ethers.formatEther(balance);
|
const balanceEth = ethers.formatEther(balance);
|
||||||
|
|
||||||
if (typeof ethers.parseEther !== 'function') {
|
|
||||||
throw new Error('Метод ethers.parseEther не найден');
|
|
||||||
}
|
|
||||||
const minBalance = ethers.parseEther("0.001");
|
const minBalance = ethers.parseEther("0.001");
|
||||||
const ok = balance >= minBalance;
|
const ok = balance >= minBalance;
|
||||||
|
|
||||||
@@ -1057,155 +1034,7 @@ class DLEV2Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Авто-верификация контракта во всех выбранных сетях через Etherscan V2
|
|
||||||
* @param {Object} args
|
|
||||||
* @param {Object} args.deployParams
|
|
||||||
* @param {Object} args.deployResult - { success, data: { dleAddress, networks: [{rpcUrl,address}] } }
|
|
||||||
* @param {string} [args.apiKey]
|
|
||||||
*/
|
|
||||||
async autoVerifyAcrossChains({ deployParams, deployResult, apiKey }) {
|
|
||||||
if (!deployResult?.success) throw new Error('Нет результата деплоя для верификации');
|
|
||||||
|
|
||||||
// Подхватить ключ из secrets, если аргумент не передан
|
|
||||||
if (!apiKey) {
|
|
||||||
try {
|
|
||||||
const { getSecret } = require('./secretStore');
|
|
||||||
apiKey = await getSecret('ETHERSCAN_V2_API_KEY');
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем компилер, standard-json-input и contractName из artifacts/build-info
|
|
||||||
const { standardJson, compilerVersion, contractName, constructorArgsHex } = await this.prepareVerificationPayload(deployParams);
|
|
||||||
|
|
||||||
// Для каждой сети отправим верификацию, используя адрес из результата для соответствующего chainId
|
|
||||||
const chainIds = Array.isArray(deployParams.supportedChainIds) ? deployParams.supportedChainIds : [];
|
|
||||||
const netMap = new Map();
|
|
||||||
if (Array.isArray(deployResult.data?.networks)) {
|
|
||||||
for (const n of deployResult.data.networks) {
|
|
||||||
if (n && typeof n.chainId === 'number') netMap.set(n.chainId, n.address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const cid of chainIds) {
|
|
||||||
try {
|
|
||||||
const addrForChain = netMap.get(cid);
|
|
||||||
if (!addrForChain) {
|
|
||||||
logger.warn(`[AutoVerify] Нет адреса для chainId=${cid} в результате деплоя, пропускаю`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const guid = await etherscanV2.submitVerification({
|
|
||||||
chainId: cid,
|
|
||||||
contractAddress: addrForChain,
|
|
||||||
contractName,
|
|
||||||
compilerVersion,
|
|
||||||
standardJsonInput: standardJson,
|
|
||||||
constructorArgsHex,
|
|
||||||
apiKey
|
|
||||||
});
|
|
||||||
logger.info(`[AutoVerify] Отправлена верификация в chainId=${cid}, guid=${guid}`);
|
|
||||||
verificationStore.updateChain(addrForChain, cid, { guid, status: 'submitted' });
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(`[AutoVerify] Ошибка отправки верификации для chainId=${cid}: ${e.message}`);
|
|
||||||
const addrForChain = netMap.get(cid) || 'unknown';
|
|
||||||
verificationStore.updateChain(addrForChain, cid, { status: `error: ${e.message}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Формирует стандартный JSON input, compilerVersion, contractName и ABI-кодированные аргументы конструктора
|
|
||||||
*/
|
|
||||||
async prepareVerificationPayload(params) {
|
|
||||||
const hre = require('hardhat');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// 1) Найти самый свежий build-info
|
|
||||||
const buildInfoDir = path.join(__dirname, '..', 'artifacts', 'build-info');
|
|
||||||
let latestFile = null;
|
|
||||||
try {
|
|
||||||
const entries = fs.readdirSync(buildInfoDir).filter(f => f.endsWith('.json'));
|
|
||||||
let bestMtime = 0;
|
|
||||||
for (const f of entries) {
|
|
||||||
const fp = path.join(buildInfoDir, f);
|
|
||||||
const st = fs.statSync(fp);
|
|
||||||
if (st.mtimeMs > bestMtime) { bestMtime = st.mtimeMs; latestFile = fp; }
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('Артефакты build-info не найдены:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
let standardJson = null;
|
|
||||||
let compilerVersion = null;
|
|
||||||
let sourcePathForDLE = 'contracts/DLE.sol';
|
|
||||||
let contractName = 'contracts/DLE.sol:DLE';
|
|
||||||
|
|
||||||
if (latestFile) {
|
|
||||||
try {
|
|
||||||
const buildInfo = JSON.parse(fs.readFileSync(latestFile, 'utf8'));
|
|
||||||
// input — это стандартный JSON input для solc
|
|
||||||
standardJson = buildInfo.input || null;
|
|
||||||
// Версия компилятора
|
|
||||||
const long = buildInfo.solcLongVersion || buildInfo.solcVersion || hre.config.solidity?.version;
|
|
||||||
compilerVersion = long ? (long.startsWith('v') ? long : `v${long}`) : undefined;
|
|
||||||
|
|
||||||
// Найти путь контракта DLE
|
|
||||||
if (buildInfo.output && buildInfo.output.contracts) {
|
|
||||||
for (const [filePathKey, contractsMap] of Object.entries(buildInfo.output.contracts)) {
|
|
||||||
if (contractsMap && contractsMap['DLE']) {
|
|
||||||
sourcePathForDLE = filePathKey;
|
|
||||||
contractName = `${filePathKey}:DLE`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('Не удалось прочитать build-info:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если не нашли — fallback на config
|
|
||||||
if (!compilerVersion) compilerVersion = `v${hre.config.solidity.compilers?.[0]?.version || hre.config.solidity.version}`;
|
|
||||||
if (!standardJson) {
|
|
||||||
// fallback минимальная структура
|
|
||||||
standardJson = {
|
|
||||||
language: 'Solidity',
|
|
||||||
sources: { [sourcePathForDLE]: { content: '' } },
|
|
||||||
settings: { optimizer: { enabled: true, runs: 200 } }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Посчитать ABI-код аргументов конструктора через сравнение с bytecode
|
|
||||||
// Конструктор: (dleConfig, currentChainId, initializer)
|
|
||||||
const Factory = await hre.ethers.getContractFactory('DLE');
|
|
||||||
const dleConfig = {
|
|
||||||
name: params.name,
|
|
||||||
symbol: params.symbol,
|
|
||||||
location: params.location,
|
|
||||||
coordinates: params.coordinates,
|
|
||||||
jurisdiction: params.jurisdiction,
|
|
||||||
okvedCodes: params.okvedCodes || [],
|
|
||||||
kpp: params.kpp,
|
|
||||||
quorumPercentage: params.quorumPercentage,
|
|
||||||
initialPartners: params.initialPartners,
|
|
||||||
initialAmounts: params.initialAmounts,
|
|
||||||
supportedChainIds: params.supportedChainIds
|
|
||||||
};
|
|
||||||
const initializer = params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000";
|
|
||||||
const deployTx = await Factory.getDeployTransaction(dleConfig, params.currentChainId, initializer);
|
|
||||||
const fullData = deployTx.data; // 0x + creation bytecode + encoded args
|
|
||||||
const bytecode = Factory.bytecode; // 0x + creation bytecode
|
|
||||||
let constructorArgsHex;
|
|
||||||
try {
|
|
||||||
if (fullData && bytecode && fullData.startsWith(bytecode)) {
|
|
||||||
constructorArgsHex = '0x' + fullData.slice(bytecode.length);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('Не удалось выделить constructorArguments из deployTx.data:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { standardJson, compilerVersion, contractName, constructorArgsHex };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new DLEV2Service();
|
module.exports = new DLEV2Service();
|
||||||
@@ -585,7 +585,7 @@ class EmailBotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendEmail(to, subject, text) {
|
async sendEmail(to, subject, text) {
|
||||||
const maxRetries = 3;
|
const maxRetries = 1;
|
||||||
const retryDelay = 5000; // 5 секунд между попытками
|
const retryDelay = 5000; // 5 секунд между попытками
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lightweight encrypted secret store over encryptedDatabaseService
|
* Lightweight encrypted secret store over encryptedDatabaseService
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Сервис получения балансов токенов пользователя из БД и RPC
|
* Сервис получения балансов токенов пользователя из БД и RPC
|
||||||
*/
|
*/
|
||||||
|
|||||||
286
backend/utils/NonceManager.js
Normal file
286
backend/utils/NonceManager.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Менеджер nonce для синхронизации транзакций в мультичейн-деплое
|
||||||
|
* Обеспечивает правильную последовательность транзакций без конфликтов
|
||||||
|
*/
|
||||||
|
class NonceManager {
|
||||||
|
constructor() {
|
||||||
|
this.nonceCache = new Map(); // Кэш nonce для каждого кошелька
|
||||||
|
this.pendingTransactions = new Map(); // Ожидающие транзакции
|
||||||
|
this.locks = new Map(); // Блокировки для предотвращения конкурентного доступа
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить актуальный nonce для кошелька в сети
|
||||||
|
* @param {string} rpcUrl - URL RPC провайдера
|
||||||
|
* @param {string} walletAddress - Адрес кошелька
|
||||||
|
* @param {boolean} usePending - Использовать pending транзакции
|
||||||
|
* @returns {Promise<number>} Актуальный nonce
|
||||||
|
*/
|
||||||
|
async getCurrentNonce(rpcUrl, walletAddress, usePending = true) {
|
||||||
|
const key = `${walletAddress}-${rpcUrl}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Создаем провайдер из rpcUrl
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true });
|
||||||
|
|
||||||
|
const nonce = await Promise.race([
|
||||||
|
provider.getTransactionCount(walletAddress, usePending ? 'pending' : 'latest'),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('Nonce timeout')), 30000))
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Получен nonce для ${walletAddress} в сети ${rpcUrl}: ${nonce}`);
|
||||||
|
return nonce;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NonceManager] Ошибка получения nonce для ${walletAddress}:`, error.message);
|
||||||
|
|
||||||
|
// Если сеть недоступна, возвращаем 0 как fallback
|
||||||
|
if (error.message.includes('network is not available') || error.message.includes('NETWORK_ERROR')) {
|
||||||
|
console.warn(`[NonceManager] Сеть недоступна, используем nonce 0 для ${walletAddress}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Заблокировать nonce для транзакции
|
||||||
|
* @param {ethers.Wallet} wallet - Кошелек
|
||||||
|
* @param {ethers.Provider} provider - Провайдер сети
|
||||||
|
* @returns {Promise<number>} Заблокированный nonce
|
||||||
|
*/
|
||||||
|
async lockNonce(rpcUrl, walletAddress) {
|
||||||
|
const key = `${walletAddress}-${rpcUrl}`;
|
||||||
|
|
||||||
|
// Ждем освобождения блокировки
|
||||||
|
while (this.locks.has(key)) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем блокировку
|
||||||
|
this.locks.set(key, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentNonce = await this.getCurrentNonce(rpcUrl, walletAddress);
|
||||||
|
const lockedNonce = currentNonce;
|
||||||
|
|
||||||
|
// Обновляем кэш
|
||||||
|
this.nonceCache.set(key, lockedNonce + 1);
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Заблокирован nonce ${lockedNonce} для ${walletAddress} в сети ${rpcUrl}`);
|
||||||
|
return lockedNonce;
|
||||||
|
} finally {
|
||||||
|
// Освобождаем блокировку
|
||||||
|
this.locks.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Освободить nonce после успешной транзакции
|
||||||
|
* @param {ethers.Wallet} wallet - Кошелек
|
||||||
|
* @param {ethers.Provider} provider - Провайдер сети
|
||||||
|
* @param {number} nonce - Использованный nonce
|
||||||
|
*/
|
||||||
|
releaseNonce(rpcUrl, walletAddress, nonce) {
|
||||||
|
const key = `${walletAddress}-${rpcUrl}`;
|
||||||
|
const cachedNonce = this.nonceCache.get(key) || 0;
|
||||||
|
|
||||||
|
if (nonce >= cachedNonce) {
|
||||||
|
this.nonceCache.set(key, nonce + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Освобожден nonce ${nonce} для ${walletAddress} в сети ${rpcUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Синхронизировать nonce между сетями
|
||||||
|
* @param {Array} networks - Массив сетей с кошельками
|
||||||
|
* @returns {Promise<number>} Синхронизированный nonce
|
||||||
|
*/
|
||||||
|
async synchronizeNonce(networks) {
|
||||||
|
console.log(`[NonceManager] Начинаем синхронизацию nonce для ${networks.length} сетей`);
|
||||||
|
|
||||||
|
// Получаем nonce для всех сетей
|
||||||
|
const nonces = await Promise.all(
|
||||||
|
networks.map(async (network, index) => {
|
||||||
|
try {
|
||||||
|
const nonce = await this.getCurrentNonce(network.rpcUrl, network.wallet.address);
|
||||||
|
console.log(`[NonceManager] Сеть ${index + 1}/${networks.length} (${network.chainId}): nonce=${nonce}`);
|
||||||
|
return { chainId: network.chainId, nonce, index };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NonceManager] Ошибка получения nonce для сети ${network.chainId}:`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Находим максимальный nonce
|
||||||
|
const maxNonce = Math.max(...nonces.map(n => n.nonce));
|
||||||
|
console.log(`[NonceManager] Максимальный nonce: ${maxNonce}`);
|
||||||
|
|
||||||
|
// Выравниваем nonce во всех сетях
|
||||||
|
for (const network of networks) {
|
||||||
|
const currentNonce = nonces.find(n => n.chainId === network.chainId)?.nonce || 0;
|
||||||
|
|
||||||
|
if (currentNonce < maxNonce) {
|
||||||
|
console.log(`[NonceManager] Выравниваем nonce в сети ${network.chainId} с ${currentNonce} до ${maxNonce}`);
|
||||||
|
await this.alignNonce(network.wallet, network.provider, currentNonce, maxNonce);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Синхронизация nonce завершена. Целевой nonce: ${maxNonce}`);
|
||||||
|
return maxNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выровнять nonce до целевого значения
|
||||||
|
* @param {ethers.Wallet} wallet - Кошелек
|
||||||
|
* @param {ethers.Provider} provider - Провайдер сети
|
||||||
|
* @param {number} currentNonce - Текущий nonce
|
||||||
|
* @param {number} targetNonce - Целевой nonce
|
||||||
|
*/
|
||||||
|
async alignNonce(wallet, provider, currentNonce, targetNonce) {
|
||||||
|
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
||||||
|
let nonce = currentNonce;
|
||||||
|
|
||||||
|
while (nonce < targetNonce) {
|
||||||
|
try {
|
||||||
|
// Получаем актуальный nonce перед каждой транзакцией
|
||||||
|
const actualNonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
|
||||||
|
if (actualNonce > nonce) {
|
||||||
|
nonce = actualNonce;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feeOverrides = await this.getFeeOverrides(provider);
|
||||||
|
const txReq = {
|
||||||
|
to: burnAddress,
|
||||||
|
value: 0n,
|
||||||
|
nonce: nonce,
|
||||||
|
gasLimit: 21000,
|
||||||
|
...feeOverrides
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Отправляем заполняющую транзакцию nonce=${nonce} в сети ${provider._network?.chainId}`);
|
||||||
|
const tx = await wallet.sendTransaction(txReq);
|
||||||
|
await tx.wait();
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Заполняющая транзакция nonce=${nonce} подтверждена в сети ${provider._network?.chainId}`);
|
||||||
|
nonce++;
|
||||||
|
|
||||||
|
// Небольшая задержка между транзакциями
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NonceManager] Ошибка заполняющей транзакции nonce=${nonce}:`, error.message);
|
||||||
|
|
||||||
|
if (error.message.includes('nonce too low')) {
|
||||||
|
// Обновляем nonce и пробуем снова
|
||||||
|
nonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить параметры комиссии для сети
|
||||||
|
* @param {ethers.Provider} provider - Провайдер сети
|
||||||
|
* @returns {Promise<Object>} Параметры комиссии
|
||||||
|
*/
|
||||||
|
async getFeeOverrides(provider) {
|
||||||
|
try {
|
||||||
|
const feeData = await provider.getFeeData();
|
||||||
|
|
||||||
|
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
|
||||||
|
return {
|
||||||
|
maxFeePerGas: feeData.maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
gasPrice: feeData.gasPrice
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[NonceManager] Ошибка получения fee data:`, error.message);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасная отправка транзакции с правильным nonce
|
||||||
|
* @param {ethers.Wallet} wallet - Кошелек
|
||||||
|
* @param {ethers.Provider} provider - Провайдер сети
|
||||||
|
* @param {Object} txData - Данные транзакции
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @returns {Promise<ethers.TransactionResponse>} Результат транзакции
|
||||||
|
*/
|
||||||
|
async sendTransactionSafely(wallet, provider, txData, maxRetries = 1) {
|
||||||
|
const rpcUrl = provider._getConnection().url;
|
||||||
|
const walletAddress = wallet.address;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
// Получаем актуальный nonce
|
||||||
|
const nonce = await this.lockNonce(rpcUrl, walletAddress);
|
||||||
|
|
||||||
|
const tx = await wallet.sendTransaction({
|
||||||
|
...txData,
|
||||||
|
nonce: nonce
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[NonceManager] Транзакция отправлена с nonce=${nonce} в сети ${provider._network?.chainId}`);
|
||||||
|
|
||||||
|
// Ждем подтверждения
|
||||||
|
await tx.wait();
|
||||||
|
|
||||||
|
// Освобождаем nonce
|
||||||
|
this.releaseNonce(rpcUrl, walletAddress, nonce);
|
||||||
|
|
||||||
|
return tx;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NonceManager] Попытка ${attempt + 1}/${maxRetries} неудачна:`, error.message);
|
||||||
|
|
||||||
|
if (error.message.includes('nonce too low') && attempt < maxRetries - 1) {
|
||||||
|
// Обновляем nonce и пробуем снова
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt === maxRetries - 1) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить кэш nonce
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.nonceCache.clear();
|
||||||
|
this.pendingTransactions.clear();
|
||||||
|
this.locks.clear();
|
||||||
|
console.log(`[NonceManager] Кэш nonce очищен`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = NonceManager;
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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 { keccak256, getAddress } = require('ethers').utils || require('ethers');
|
const { keccak256, getAddress } = require('ethers').utils || require('ethers');
|
||||||
|
|
||||||
function toBytes(hex) {
|
function toBytes(hex) {
|
||||||
|
|||||||
239
backend/utils/deploymentTracker.js
Normal file
239
backend/utils/deploymentTracker.js
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
/**
|
||||||
|
* 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 crypto = require('crypto');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
class DeploymentTracker extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.deployments = new Map(); // В продакшене использовать Redis
|
||||||
|
this.logger = require('../utils/logger');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создать новый деплой
|
||||||
|
createDeployment(params) {
|
||||||
|
const deploymentId = this.generateDeploymentId();
|
||||||
|
|
||||||
|
const deployment = {
|
||||||
|
id: deploymentId,
|
||||||
|
status: 'pending',
|
||||||
|
stage: 'initializing',
|
||||||
|
progress: 0,
|
||||||
|
startedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
params,
|
||||||
|
networks: {},
|
||||||
|
logs: [],
|
||||||
|
result: null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.deployments.set(deploymentId, deployment);
|
||||||
|
this.logger.info(`📝 Создан новый деплой: ${deploymentId}`);
|
||||||
|
|
||||||
|
return deploymentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получить статус деплоя
|
||||||
|
getDeployment(deploymentId) {
|
||||||
|
return this.deployments.get(deploymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновить статус деплоя
|
||||||
|
updateDeployment(deploymentId, updates) {
|
||||||
|
const deployment = this.deployments.get(deploymentId);
|
||||||
|
if (!deployment) {
|
||||||
|
this.logger.error(`❌ Деплой не найден: ${deploymentId}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(deployment, updates, { updatedAt: new Date() });
|
||||||
|
this.deployments.set(deploymentId, deployment);
|
||||||
|
|
||||||
|
// Отправляем событие через WebSocket
|
||||||
|
this.emit('deployment_updated', {
|
||||||
|
deploymentId,
|
||||||
|
...updates
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавить лог
|
||||||
|
addLog(deploymentId, message, type = 'info') {
|
||||||
|
const deployment = this.deployments.get(deploymentId);
|
||||||
|
if (!deployment) return false;
|
||||||
|
|
||||||
|
const logEntry = {
|
||||||
|
timestamp: new Date(),
|
||||||
|
message,
|
||||||
|
type
|
||||||
|
};
|
||||||
|
|
||||||
|
deployment.logs.push(logEntry);
|
||||||
|
deployment.updatedAt = new Date();
|
||||||
|
|
||||||
|
// Отправляем только лог через WebSocket (без дублирования)
|
||||||
|
this.emit('deployment_updated', {
|
||||||
|
deploymentId,
|
||||||
|
type: 'deployment_log',
|
||||||
|
log: logEntry
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновить статус сети
|
||||||
|
updateNetworkStatus(deploymentId, network, status, address = null, message = null) {
|
||||||
|
const deployment = this.deployments.get(deploymentId);
|
||||||
|
if (!deployment) return false;
|
||||||
|
|
||||||
|
deployment.networks[network] = {
|
||||||
|
status,
|
||||||
|
address,
|
||||||
|
message,
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
deployment.updatedAt = new Date();
|
||||||
|
|
||||||
|
// Отправляем обновление через WebSocket
|
||||||
|
this.emit('deployment_updated', {
|
||||||
|
deploymentId,
|
||||||
|
type: 'deployment_network_update',
|
||||||
|
network,
|
||||||
|
status,
|
||||||
|
address,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновить прогресс
|
||||||
|
updateProgress(deploymentId, stage, progress, message = null) {
|
||||||
|
const updates = {
|
||||||
|
stage,
|
||||||
|
progress,
|
||||||
|
status: progress >= 100 ? 'completed' : 'in_progress'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновляем без отправки события (только внутреннее обновление)
|
||||||
|
const deployment = this.deployments.get(deploymentId);
|
||||||
|
if (deployment) {
|
||||||
|
Object.assign(deployment, updates, { updatedAt: new Date() });
|
||||||
|
this.deployments.set(deploymentId, deployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Лог добавляется через updateDeployment, не дублируем событие
|
||||||
|
}
|
||||||
|
|
||||||
|
// Завершить деплой успешно
|
||||||
|
completeDeployment(deploymentId, result) {
|
||||||
|
const updates = {
|
||||||
|
status: 'completed',
|
||||||
|
progress: 100,
|
||||||
|
result,
|
||||||
|
completedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateDeployment(deploymentId, updates);
|
||||||
|
|
||||||
|
// Событие уже отправлено через updateDeployment
|
||||||
|
|
||||||
|
this.logger.info(`✅ Деплой завершен: ${deploymentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Завершить деплой с ошибкой
|
||||||
|
failDeployment(deploymentId, error) {
|
||||||
|
const updates = {
|
||||||
|
status: 'failed',
|
||||||
|
error: error.message || error,
|
||||||
|
failedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateDeployment(deploymentId, updates);
|
||||||
|
|
||||||
|
// Событие уже отправлено через updateDeployment
|
||||||
|
|
||||||
|
this.logger.error(`❌ Деплой провален: ${deploymentId}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очистить старые деплои (вызывать по крону)
|
||||||
|
cleanupOldDeployments(olderThanHours = 24) {
|
||||||
|
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
|
||||||
|
let cleaned = 0;
|
||||||
|
|
||||||
|
for (const [id, deployment] of this.deployments.entries()) {
|
||||||
|
if (deployment.updatedAt < cutoff && ['completed', 'failed'].includes(deployment.status)) {
|
||||||
|
this.deployments.delete(id);
|
||||||
|
cleaned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaned > 0) {
|
||||||
|
this.logger.info(`🧹 Очищено ${cleaned} старых деплоев`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сгенерировать уникальный ID
|
||||||
|
generateDeploymentId() {
|
||||||
|
return `deploy_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получить все активные деплои
|
||||||
|
getActiveDeployments() {
|
||||||
|
const active = [];
|
||||||
|
for (const deployment of this.deployments.values()) {
|
||||||
|
if (['pending', 'in_progress'].includes(deployment.status)) {
|
||||||
|
active.push(deployment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получить статистику
|
||||||
|
getStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.deployments.size,
|
||||||
|
pending: 0,
|
||||||
|
inProgress: 0,
|
||||||
|
completed: 0,
|
||||||
|
failed: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const deployment of this.deployments.values()) {
|
||||||
|
switch (deployment.status) {
|
||||||
|
case 'pending':
|
||||||
|
stats.pending++;
|
||||||
|
break;
|
||||||
|
case 'in_progress':
|
||||||
|
stats.inProgress++;
|
||||||
|
break;
|
||||||
|
case 'completed':
|
||||||
|
stats.completed++;
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
stats.failed++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton экземпляр
|
||||||
|
const deploymentTracker = new DeploymentTracker();
|
||||||
|
|
||||||
|
module.exports = deploymentTracker;
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const tokenBalanceService = require('./services/tokenBalanceService');
|
const tokenBalanceService = require('./services/tokenBalanceService');
|
||||||
|
const deploymentTracker = require('./utils/deploymentTracker');
|
||||||
|
|
||||||
let wss = null;
|
let wss = null;
|
||||||
// Храним клиентов по userId для персонализированных уведомлений
|
// Храним клиентов по userId для персонализированных уведомлений
|
||||||
@@ -28,6 +29,11 @@ const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
|
|||||||
function initWSS(server) {
|
function initWSS(server) {
|
||||||
wss = new WebSocket.Server({ server, path: '/ws' });
|
wss = new WebSocket.Server({ server, path: '/ws' });
|
||||||
|
|
||||||
|
// Подключаем deployment tracker к WebSocket
|
||||||
|
deploymentTracker.on('deployment_updated', (data) => {
|
||||||
|
broadcastDeploymentUpdate(data);
|
||||||
|
});
|
||||||
|
|
||||||
wss.on('connection', (ws, req) => {
|
wss.on('connection', (ws, req) => {
|
||||||
// console.log('🔌 [WebSocket] Новое подключение');
|
// console.log('🔌 [WebSocket] Новое подключение');
|
||||||
// console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress);
|
// console.log('🔌 [WebSocket] IP клиента:', req.socket.remoteAddress);
|
||||||
@@ -451,6 +457,29 @@ function broadcastTokenBalanceChanged(userId, tokenAddress, newBalance, network)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функции для деплоя
|
||||||
|
function broadcastDeploymentUpdate(data) {
|
||||||
|
if (!wss) return;
|
||||||
|
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'deployment_update',
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отправляем всем подключенным клиентам
|
||||||
|
wss.clients.forEach(client => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
client.send(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WebSocket] Ошибка при отправке deployment update:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📡 [WebSocket] Отправлено deployment update: ${data.type || 'unknown'}`);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initWSS,
|
initWSS,
|
||||||
broadcastContactsUpdate,
|
broadcastContactsUpdate,
|
||||||
@@ -469,6 +498,7 @@ module.exports = {
|
|||||||
broadcastAuthTokenUpdated,
|
broadcastAuthTokenUpdated,
|
||||||
broadcastTokenBalancesUpdate,
|
broadcastTokenBalancesUpdate,
|
||||||
broadcastTokenBalanceChanged,
|
broadcastTokenBalanceChanged,
|
||||||
|
broadcastDeploymentUpdate,
|
||||||
getConnectedUsers,
|
getConnectedUsers,
|
||||||
getStats
|
getStats
|
||||||
};
|
};
|
||||||
|
|||||||
617
backup_deploy_20250922_220227/deploy-multichain.js
Normal file
617
backup_deploy_20250922_220227/deploy-multichain.js
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
const hre = require('hardhat');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Подбираем безопасные 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit) {
|
||||||
|
const { ethers } = hre;
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const wallet = new ethers.Wallet(pk, provider);
|
||||||
|
const net = await provider.getNetwork();
|
||||||
|
|
||||||
|
// 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)}...`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[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}`);
|
||||||
|
|
||||||
|
if (current > targetDLENonce) {
|
||||||
|
throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current < targetDLENonce) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`);
|
||||||
|
|
||||||
|
// Используем burn address для более надежных транзакций
|
||||||
|
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
||||||
|
|
||||||
|
while (current < targetDLENonce) {
|
||||||
|
const overrides = await getFeeOverrides(provider);
|
||||||
|
let gasLimit = 21000; // минимальный gas для обычной транзакции
|
||||||
|
let sent = false;
|
||||||
|
let lastErr = null;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 3 && !sent; attempt++) {
|
||||||
|
try {
|
||||||
|
const txReq = {
|
||||||
|
to: burnAddress, // отправляем на burn address вместо своего адреса
|
||||||
|
value: 0n,
|
||||||
|
nonce: current,
|
||||||
|
gasLimit,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
|
||||||
|
const txFill = await wallet.sendTransaction(txReq);
|
||||||
|
await txFill.wait();
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`);
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) {
|
||||||
|
gasLimit = 50000; // увеличиваем gas limit
|
||||||
|
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}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
console.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}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[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}`);
|
||||||
|
|
||||||
|
const feeOverrides = await getFeeOverrides(provider);
|
||||||
|
let gasLimit;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Оцениваем газ для деплоя DLE
|
||||||
|
const est = await wallet.estimateGas({ data: dleInit, ...feeOverrides }).catch(() => null);
|
||||||
|
|
||||||
|
// Рассчитываем доступный gasLimit из баланса
|
||||||
|
const balance = await provider.getBalance(wallet.address, 'latest');
|
||||||
|
const effPrice = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n;
|
||||||
|
const reserve = hre.ethers.parseEther('0.005');
|
||||||
|
const maxByBalance = effPrice > 0n && balance > reserve ? (balance - reserve) / effPrice : 3_000_000n;
|
||||||
|
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()}`);
|
||||||
|
} catch (_) {
|
||||||
|
gasLimit = 3_000_000n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем предсказанный адрес DLE
|
||||||
|
const predictedAddress = ethers.getCreateAddress({
|
||||||
|
from: wallet.address,
|
||||||
|
nonce: targetDLENonce
|
||||||
|
});
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress}`);
|
||||||
|
|
||||||
|
// Проверяем, не развернут ли уже контракт
|
||||||
|
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`);
|
||||||
|
return { address: predictedAddress, chainId: Number(net.chainId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Деплоим DLE
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rc = await tx.wait();
|
||||||
|
const deployedAddress = rc.contractAddress || predictedAddress;
|
||||||
|
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`);
|
||||||
|
return { address: deployedAddress, chainId: Number(net.chainId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Деплой модулей в одной сети
|
||||||
|
async function deployModulesInNetwork(rpcUrl, pk, dleAddress, params) {
|
||||||
|
const { ethers } = hre;
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const wallet = new ethers.Wallet(pk, provider);
|
||||||
|
const net = await provider.getNetwork();
|
||||||
|
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying modules...`);
|
||||||
|
|
||||||
|
const modules = {};
|
||||||
|
|
||||||
|
// Получаем начальный nonce для всех модулей
|
||||||
|
let currentNonce = await wallet.getNonce();
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce for modules: ${currentNonce}`);
|
||||||
|
|
||||||
|
// Функция для безопасного деплоя с правильным nonce
|
||||||
|
async function deployWithNonce(contractFactory, args, moduleName) {
|
||||||
|
try {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying ${moduleName} with nonce: ${currentNonce}`);
|
||||||
|
|
||||||
|
// Проверяем, что nonce актуален
|
||||||
|
const actualNonce = await wallet.getNonce();
|
||||||
|
if (actualNonce > currentNonce) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch, updating from ${currentNonce} to ${actualNonce}`);
|
||||||
|
currentNonce = actualNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = await contractFactory.connect(wallet).deploy(...args);
|
||||||
|
await contract.waitForDeployment();
|
||||||
|
const address = await contract.getAddress();
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployed at: ${address}`);
|
||||||
|
currentNonce++;
|
||||||
|
return address;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployment failed:`, error.message);
|
||||||
|
// Даже при ошибке увеличиваем nonce, чтобы не было конфликтов
|
||||||
|
currentNonce++;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Деплой TreasuryModule
|
||||||
|
const TreasuryModule = await hre.ethers.getContractFactory('TreasuryModule');
|
||||||
|
modules.treasuryModule = await deployWithNonce(
|
||||||
|
TreasuryModule,
|
||||||
|
[dleAddress, Number(net.chainId), wallet.address], // _dleContract, _chainId, _emergencyAdmin
|
||||||
|
'TreasuryModule'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Деплой TimelockModule
|
||||||
|
const TimelockModule = await hre.ethers.getContractFactory('TimelockModule');
|
||||||
|
modules.timelockModule = await deployWithNonce(
|
||||||
|
TimelockModule,
|
||||||
|
[dleAddress], // _dleContract
|
||||||
|
'TimelockModule'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Деплой DLEReader
|
||||||
|
const DLEReader = await hre.ethers.getContractFactory('DLEReader');
|
||||||
|
modules.dleReader = await deployWithNonce(
|
||||||
|
DLEReader,
|
||||||
|
[dleAddress], // _dleContract
|
||||||
|
'DLEReader'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Инициализация модулей в DLE
|
||||||
|
try {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing modules in DLE with nonce: ${currentNonce}`);
|
||||||
|
|
||||||
|
// Проверяем, что nonce актуален
|
||||||
|
const actualNonce = await wallet.getNonce();
|
||||||
|
if (actualNonce > currentNonce) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before module init, updating from ${currentNonce} to ${actualNonce}`);
|
||||||
|
currentNonce = actualNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet);
|
||||||
|
|
||||||
|
// Проверяем, что все модули задеплоены
|
||||||
|
const treasuryAddress = modules.treasuryModule;
|
||||||
|
const timelockAddress = modules.timelockModule;
|
||||||
|
const readerAddress = modules.dleReader;
|
||||||
|
|
||||||
|
if (treasuryAddress && timelockAddress && readerAddress) {
|
||||||
|
// Инициализация базовых модулей
|
||||||
|
await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`);
|
||||||
|
currentNonce++;
|
||||||
|
} else {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message);
|
||||||
|
// Даже при ошибке увеличиваем nonce
|
||||||
|
currentNonce++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация logoURI
|
||||||
|
try {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI with nonce: ${currentNonce}`);
|
||||||
|
|
||||||
|
// Проверяем, что nonce актуален
|
||||||
|
const actualNonce = await wallet.getNonce();
|
||||||
|
if (actualNonce > currentNonce) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before logoURI init, updating from ${currentNonce} to ${actualNonce}`);
|
||||||
|
currentNonce = actualNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем логотип из параметров деплоя или fallback
|
||||||
|
const logoURL = params.logoURI || "https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE";
|
||||||
|
const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet);
|
||||||
|
await dleContract.initializeLogoURI(logoURL);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized: ${logoURL}`);
|
||||||
|
currentNonce++;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${e.message}`);
|
||||||
|
// Fallback на базовый логотип
|
||||||
|
try {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} trying fallback logoURI with nonce: ${currentNonce}`);
|
||||||
|
const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet);
|
||||||
|
await dleContract.initializeLogoURI("https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE");
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI initialized`);
|
||||||
|
currentNonce++;
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI also failed: ${fallbackError.message}`);
|
||||||
|
// Даже при ошибке увеличиваем nonce
|
||||||
|
currentNonce++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Деплой модулей во всех сетях
|
||||||
|
async function deployModulesInAllNetworks(networks, pk, dleAddress, params) {
|
||||||
|
const moduleResults = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < networks.length; i++) {
|
||||||
|
const rpcUrl = networks[i];
|
||||||
|
console.log(`[MULTI_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const modules = await deployModulesInNetwork(rpcUrl, pk, dleAddress, params);
|
||||||
|
moduleResults.push(modules);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MULTI_DBG] Failed to deploy modules in network ${i + 1}:`, error.message);
|
||||||
|
moduleResults.push({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moduleResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Верификация контрактов в одной сети
|
||||||
|
async function verifyContractsInNetwork(rpcUrl, pk, dleAddress, modules, params) {
|
||||||
|
const { ethers } = hre;
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const wallet = new ethers.Wallet(pk, provider);
|
||||||
|
const net = await provider.getNetwork();
|
||||||
|
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting verification...`);
|
||||||
|
|
||||||
|
const verification = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Верификация DLE
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying DLE...`);
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: dleAddress,
|
||||||
|
constructorArguments: [
|
||||||
|
{
|
||||||
|
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)),
|
||||||
|
supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
|
||||||
|
},
|
||||||
|
BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1),
|
||||||
|
params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000"
|
||||||
|
],
|
||||||
|
});
|
||||||
|
verification.dle = 'success';
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification successful`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification failed: ${error.message}`);
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification error details:`, error);
|
||||||
|
verification.dle = 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Верификация модулей
|
||||||
|
if (modules && !modules.error) {
|
||||||
|
try {
|
||||||
|
// Верификация TreasuryModule
|
||||||
|
if (modules.treasuryModule) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying TreasuryModule...`);
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: modules.treasuryModule,
|
||||||
|
constructorArguments: [
|
||||||
|
dleAddress, // _dleContract
|
||||||
|
Number(net.chainId), // _chainId
|
||||||
|
wallet.address // _emergencyAdmin
|
||||||
|
],
|
||||||
|
});
|
||||||
|
verification.treasuryModule = 'success';
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule verification successful`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule verification failed: ${error.message}`);
|
||||||
|
verification.treasuryModule = 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Верификация TimelockModule
|
||||||
|
if (modules.timelockModule) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying TimelockModule...`);
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: modules.timelockModule,
|
||||||
|
constructorArguments: [
|
||||||
|
dleAddress // _dleContract
|
||||||
|
],
|
||||||
|
});
|
||||||
|
verification.timelockModule = 'success';
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule verification successful`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule verification failed: ${error.message}`);
|
||||||
|
verification.timelockModule = 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Верификация DLEReader
|
||||||
|
if (modules.dleReader) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying DLEReader...`);
|
||||||
|
await hre.run("verify:verify", {
|
||||||
|
address: modules.dleReader,
|
||||||
|
constructorArguments: [
|
||||||
|
dleAddress // _dleContract
|
||||||
|
],
|
||||||
|
});
|
||||||
|
verification.dleReader = 'success';
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader verification successful`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader verification failed: ${error.message}`);
|
||||||
|
verification.dleReader = 'failed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verification;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Верификация контрактов во всех сетях
|
||||||
|
async function verifyContractsInAllNetworks(networks, pk, dleAddress, moduleResults, params) {
|
||||||
|
const verificationResults = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < networks.length; i++) {
|
||||||
|
const rpcUrl = networks[i];
|
||||||
|
console.log(`[MULTI_DBG] verifying contracts in network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verification = await verifyContractsInNetwork(rpcUrl, pk, dleAddress, moduleResults[i], params);
|
||||||
|
verificationResults.push(verification);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MULTI_DBG] Failed to verify contracts in network ${i + 1}:`, error.message);
|
||||||
|
verificationResults.push({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { ethers } = hre;
|
||||||
|
|
||||||
|
// Загружаем параметры из файла
|
||||||
|
const paramsPath = path.join(__dirname, './current-params.json');
|
||||||
|
if (!fs.existsSync(paramsPath)) {
|
||||||
|
throw new Error('Файл параметров не найден: ' + paramsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8'));
|
||||||
|
console.log('[MULTI_DBG] Загружены параметры:', {
|
||||||
|
name: params.name,
|
||||||
|
symbol: params.symbol,
|
||||||
|
supportedChainIds: params.supportedChainIds,
|
||||||
|
CREATE2_SALT: params.CREATE2_SALT
|
||||||
|
});
|
||||||
|
|
||||||
|
const pk = process.env.PRIVATE_KEY;
|
||||||
|
const salt = params.CREATE2_SALT;
|
||||||
|
const networks = params.rpcUrls || [];
|
||||||
|
|
||||||
|
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('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)),
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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)}...`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('[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));
|
||||||
|
const nonces = [];
|
||||||
|
for (let i = 0; i < providers.length; i++) {
|
||||||
|
const n = await providers[i].getTransactionCount(wallets[i].address, 'pending');
|
||||||
|
nonces.push(n);
|
||||||
|
}
|
||||||
|
const targetDLENonce = Math.max(...nonces);
|
||||||
|
console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`);
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < networks.length; i++) {
|
||||||
|
const rpcUrl = networks[i];
|
||||||
|
console.log(`[MULTI_DBG] deploying to network ${i + 1}/${networks.length}: ${rpcUrl}`);
|
||||||
|
const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit);
|
||||||
|
results.push({ rpcUrl, ...r });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что все адреса одинаковые
|
||||||
|
const addresses = results.map(r => r.address);
|
||||||
|
const uniqueAddresses = [...new Set(addresses)];
|
||||||
|
|
||||||
|
if (uniqueAddresses.length > 1) {
|
||||||
|
console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!');
|
||||||
|
console.error('[MULTI_DBG] addresses:', uniqueAddresses);
|
||||||
|
throw new Error('Nonce alignment failed - addresses are different');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]);
|
||||||
|
|
||||||
|
// Деплой модулей во всех сетях
|
||||||
|
console.log('[MULTI_DBG] Starting module deployment...');
|
||||||
|
const moduleResults = await deployModulesInAllNetworks(networks, pk, uniqueAddresses[0], params);
|
||||||
|
|
||||||
|
// Верификация контрактов
|
||||||
|
console.log('[MULTI_DBG] Starting contract verification...');
|
||||||
|
const verificationResults = await verifyContractsInAllNetworks(networks, pk, uniqueAddresses[0], moduleResults, params);
|
||||||
|
|
||||||
|
// Объединяем результаты
|
||||||
|
const finalResults = results.map((result, index) => ({
|
||||||
|
...result,
|
||||||
|
modules: moduleResults[index] || {},
|
||||||
|
verification: verificationResults[index] || {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults));
|
||||||
|
|
||||||
|
// Сохраняем информацию о модулях в отдельный файл для каждого DLE
|
||||||
|
// Добавляем информацию о сетях (chainId, rpcUrl)
|
||||||
|
const modulesInfo = {
|
||||||
|
dleAddress: uniqueAddresses[0],
|
||||||
|
networks: networks.map((rpcUrl, index) => ({
|
||||||
|
rpcUrl: rpcUrl,
|
||||||
|
chainId: null, // Будет заполнено ниже
|
||||||
|
networkName: null // Будет заполнено ниже
|
||||||
|
})),
|
||||||
|
modules: moduleResults,
|
||||||
|
verification: verificationResults,
|
||||||
|
deployTimestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Получаем chainId для каждой сети
|
||||||
|
for (let i = 0; i < networks.length; i++) {
|
||||||
|
try {
|
||||||
|
const provider = new hre.ethers.JsonRpcProvider(networks[i]);
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
modulesInfo.networks[i].chainId = Number(network.chainId);
|
||||||
|
|
||||||
|
// Определяем название сети по chainId
|
||||||
|
const networkNames = {
|
||||||
|
1: 'Ethereum Mainnet',
|
||||||
|
5: 'Goerli',
|
||||||
|
11155111: 'Sepolia',
|
||||||
|
137: 'Polygon Mainnet',
|
||||||
|
80001: 'Mumbai',
|
||||||
|
56: 'BSC Mainnet',
|
||||||
|
97: 'BSC Testnet',
|
||||||
|
42161: 'Arbitrum One',
|
||||||
|
421614: 'Arbitrum Sepolia',
|
||||||
|
10: 'Optimism',
|
||||||
|
11155420: 'Optimism Sepolia',
|
||||||
|
8453: 'Base',
|
||||||
|
84532: 'Base Sepolia'
|
||||||
|
};
|
||||||
|
modulesInfo.networks[i].networkName = networkNames[Number(network.chainId)] || `Chain ID ${Number(network.chainId)}`;
|
||||||
|
|
||||||
|
console.log(`[MULTI_DBG] Сеть ${i + 1}: chainId=${Number(network.chainId)}, name=${modulesInfo.networks[i].networkName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[MULTI_DBG] Ошибка получения chainId для сети ${i + 1}:`, error.message);
|
||||||
|
modulesInfo.networks[i].chainId = null;
|
||||||
|
modulesInfo.networks[i].networkName = `Сеть ${i + 1}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию temp если её нет
|
||||||
|
const tempDir = path.join(__dirname, '../temp');
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployResultPath = path.join(tempDir, `modules-${uniqueAddresses[0].toLowerCase()}.json`);
|
||||||
|
fs.writeFileSync(deployResultPath, JSON.stringify(modulesInfo, null, 2));
|
||||||
|
console.log(`[MULTI_DBG] Modules info saved to: ${deployResultPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error(e); process.exit(1); });
|
||||||
|
|
||||||
|
|
||||||
452
backup_deploy_20250922_220227/dleV2.js
Normal file
452
backup_deploy_20250922_220227/dleV2.js
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
/**
|
||||||
|
* 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 dleV2Service = require('../services/dleV2Service');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const auth = require('../middleware/auth');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const ethers = require('ethers'); // Added ethers for private key validation
|
||||||
|
const create2 = require('../utils/create2');
|
||||||
|
const verificationStore = require('../services/verificationStore');
|
||||||
|
const etherscanV2 = require('../services/etherscanV2VerificationService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/dle-v2
|
||||||
|
* @desc Создать новое DLE v2 (Digital Legal Entity)
|
||||||
|
* @access Private (только для авторизованных пользователей с ролью admin)
|
||||||
|
*/
|
||||||
|
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const dleParams = req.body;
|
||||||
|
logger.info('Получен запрос на создание DLE v2:', dleParams);
|
||||||
|
|
||||||
|
// Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя
|
||||||
|
if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) {
|
||||||
|
// Проверяем, есть ли в сессии адрес кошелька пользователя
|
||||||
|
if (!req.user || !req.user.walletAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем адрес авторизованного пользователя
|
||||||
|
dleParams.initialPartners = [req.user.address || req.user.walletAddress];
|
||||||
|
|
||||||
|
// Если суммы не указаны, используем значение по умолчанию (100% токенов)
|
||||||
|
if (!dleParams.initialAmounts || dleParams.initialAmounts.length === 0) {
|
||||||
|
dleParams.initialAmounts = ['1000000000000000000000000']; // 1,000,000 токенов
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем DLE v2
|
||||||
|
const result = await dleV2Service.createDLE(dleParams);
|
||||||
|
|
||||||
|
logger.info('DLE v2 успешно создано:', result);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'DLE v2 успешно создано',
|
||||||
|
data: result.data
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при создании DLE v2:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при создании DLE v2'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/dle-v2
|
||||||
|
* @desc Получить список всех DLE v2
|
||||||
|
* @access Public (доступно всем пользователям)
|
||||||
|
*/
|
||||||
|
router.get('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const dles = dleV2Service.getAllDLEs();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: dles
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении списка DLE v2:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при получении списка DLE v2'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/dle-v2/default-params
|
||||||
|
* @desc Получить параметры по умолчанию для создания DLE v2
|
||||||
|
* @access Private
|
||||||
|
*/
|
||||||
|
router.get('/default-params', auth.requireAuth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const defaultParams = {
|
||||||
|
name: '',
|
||||||
|
symbol: '',
|
||||||
|
location: '',
|
||||||
|
coordinates: '',
|
||||||
|
jurisdiction: 1,
|
||||||
|
oktmo: 45000000000,
|
||||||
|
okvedCodes: [],
|
||||||
|
kpp: 770101001,
|
||||||
|
quorumPercentage: 51,
|
||||||
|
initialPartners: [],
|
||||||
|
initialAmounts: [],
|
||||||
|
supportedChainIds: [1, 137, 56, 42161],
|
||||||
|
currentChainId: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: defaultParams
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении параметров по умолчанию:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при получении параметров по умолчанию'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route DELETE /api/dle-v2/:dleAddress
|
||||||
|
* @desc Удалить DLE v2 по адресу
|
||||||
|
* @access Private (только для авторизованных пользователей с ролью admin)
|
||||||
|
*/
|
||||||
|
router.delete('/:dleAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress } = req.params;
|
||||||
|
logger.info(`Получен запрос на удаление DLE v2 с адресом: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Проверяем существование DLE v2 в директории contracts-data/dles
|
||||||
|
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||||
|
const files = fs.readdirSync(dlesDir);
|
||||||
|
|
||||||
|
let fileToDelete = null;
|
||||||
|
|
||||||
|
// Находим файл, содержащий указанный адрес DLE
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.includes('dle-v2-') && file.endsWith('.json')) {
|
||||||
|
const filePath = path.join(dlesDir, file);
|
||||||
|
if (fs.statSync(filePath).isFile()) {
|
||||||
|
try {
|
||||||
|
const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
if (dleData.dleAddress && dleData.dleAddress.toLowerCase() === dleAddress.toLowerCase()) {
|
||||||
|
fileToDelete = filePath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Ошибка при чтении файла ${file}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileToDelete) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: `DLE v2 с адресом ${dleAddress} не найдено`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файл
|
||||||
|
fs.unlinkSync(fileToDelete);
|
||||||
|
|
||||||
|
logger.info(`DLE v2 с адресом ${dleAddress} успешно удалено`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `DLE v2 с адресом ${dleAddress} успешно удалено`
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при удалении DLE v2:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при удалении DLE v2'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/dle-v2/check-admin-tokens
|
||||||
|
* @desc Проверить баланс админских токенов для адреса
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.get('/check-admin-tokens', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { address } = req.query;
|
||||||
|
|
||||||
|
if (!address) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Адрес кошелька не передан'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем баланс токенов
|
||||||
|
const { checkAdminRole } = require('../services/admin-role');
|
||||||
|
const isAdmin = await checkAdminRole(address);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
address: address,
|
||||||
|
message: isAdmin ? 'Админские токены найдены' : 'Админские токены не найдены'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при проверке админских токенов:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при проверке админских токенов'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route POST /api/dle-v2/validate-private-key
|
||||||
|
* @desc Валидировать приватный ключ и получить адрес кошелька
|
||||||
|
* @access Public
|
||||||
|
*/
|
||||||
|
router.post('/validate-private-key', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { privateKey } = req.body;
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Приватный ключ не передан'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Логируем входящий ключ (только для отладки)
|
||||||
|
logger.info('Получен приватный ключ для валидации:', privateKey);
|
||||||
|
logger.info('Длина входящего ключа:', privateKey.length);
|
||||||
|
logger.info('Тип входящего ключа:', typeof privateKey);
|
||||||
|
logger.info('Полный объект запроса:', JSON.stringify(req.body));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Очищаем ключ от префикса 0x если есть
|
||||||
|
const cleanKey = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
|
||||||
|
|
||||||
|
// Логируем очищенный ключ (только для отладки)
|
||||||
|
logger.info('Очищенный ключ:', cleanKey);
|
||||||
|
logger.info('Длина очищенного ключа:', cleanKey.length);
|
||||||
|
|
||||||
|
// Проверяем длину и формат (64 символа в hex)
|
||||||
|
if (cleanKey.length !== 64 || !/^[a-fA-F0-9]+$/.test(cleanKey)) {
|
||||||
|
logger.error('Некорректный формат ключа. Длина:', cleanKey.length, 'Формат:', /^[a-fA-F0-9]+$/.test(cleanKey));
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Некорректный формат приватного ключа'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем адрес из приватного ключа
|
||||||
|
const wallet = new ethers.Wallet('0x' + cleanKey);
|
||||||
|
const address = wallet.address;
|
||||||
|
|
||||||
|
// Логируем сгенерированный адрес
|
||||||
|
logger.info('Сгенерированный адрес из приватного ключа:', address);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
isValid: true,
|
||||||
|
address: address,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при генерации адреса из приватного ключа:', error);
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Некорректный приватный ключ'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при валидации приватного ключа:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Произошла ошибка при валидации приватного ключа'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Дополнительные маршруты (подключаются из app.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Сохранить GUID верификации (если нужно отдельным вызовом)
|
||||||
|
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address, chainId, guid } = req.body || {};
|
||||||
|
if (!address || !chainId || !guid) return res.status(400).json({ success: false, message: 'address, chainId, guid обязательны' });
|
||||||
|
const data = verificationStore.updateChain(address, chainId, { guid, status: 'submitted' });
|
||||||
|
return res.json({ success: true, data });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получить статусы верификации по адресу DLE
|
||||||
|
router.get('/verify/status/:address', auth.requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { address } = req.params;
|
||||||
|
const data = verificationStore.read(address);
|
||||||
|
return res.json({ success: true, data });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновить статусы верификации, опросив Etherscan V2
|
||||||
|
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 data = verificationStore.read(address);
|
||||||
|
if (!data || !data.chains) return res.json({ success: true, data });
|
||||||
|
|
||||||
|
// Если guid отсутствует или ранее была ошибка chainid — попробуем автоматически переотправить верификацию (resubmit)
|
||||||
|
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 card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
|
||||||
|
if (card) {
|
||||||
|
const deployParams = {
|
||||||
|
name: card.name,
|
||||||
|
symbol: card.symbol,
|
||||||
|
location: card.location,
|
||||||
|
coordinates: card.coordinates,
|
||||||
|
jurisdiction: card.jurisdiction,
|
||||||
|
oktmo: card.oktmo,
|
||||||
|
okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [],
|
||||||
|
kpp: card.kpp,
|
||||||
|
quorumPercentage: card.quorumPercentage,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
|
||||||
|
try {
|
||||||
|
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Далее — обычный опрос по имеющимся guid
|
||||||
|
const latest = verificationStore.read(address);
|
||||||
|
const chains = Object.values(latest.chains);
|
||||||
|
for (const c of chains) {
|
||||||
|
if (!c.guid || !c.chainId) continue;
|
||||||
|
try {
|
||||||
|
const st = await etherscanV2.checkStatus(c.chainId, c.guid, etherscanApiKey);
|
||||||
|
verificationStore.updateChain(address, c.chainId, { status: st?.result || st?.message || 'unknown' });
|
||||||
|
} catch (e) {
|
||||||
|
verificationStore.updateChain(address, c.chainId, { status: `error: ${e.message}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updated = verificationStore.read(address);
|
||||||
|
return res.json({ success: true, data: updated });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Повторно отправить верификацию на Etherscan V2 для уже созданного DLE
|
||||||
|
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) {
|
||||||
|
return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' });
|
||||||
|
}
|
||||||
|
// Найти карточку DLE по адресу
|
||||||
|
const list = dleV2Service.getAllDLEs();
|
||||||
|
const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase());
|
||||||
|
if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' });
|
||||||
|
|
||||||
|
// Сформировать deployParams из карточки
|
||||||
|
const deployParams = {
|
||||||
|
name: card.name,
|
||||||
|
symbol: card.symbol,
|
||||||
|
location: card.location,
|
||||||
|
coordinates: card.coordinates,
|
||||||
|
jurisdiction: card.jurisdiction,
|
||||||
|
oktmo: card.oktmo,
|
||||||
|
okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [],
|
||||||
|
kpp: card.kpp,
|
||||||
|
quorumPercentage: card.quorumPercentage,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сформировать deployResult из карточки
|
||||||
|
const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } };
|
||||||
|
|
||||||
|
await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey });
|
||||||
|
const updated = verificationStore.read(address);
|
||||||
|
return res.json({ success: true, data: updated });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Предварительная проверка балансов во всех выбранных сетях
|
||||||
|
router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { supportedChainIds, privateKey } = req.body || {};
|
||||||
|
if (!privateKey) return res.status(400).json({ success: false, message: 'Приватный ключ не передан' });
|
||||||
|
if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' });
|
||||||
|
}
|
||||||
|
const result = await dleV2Service.checkBalances(supportedChainIds, privateKey);
|
||||||
|
return res.json({ success: true, data: result });
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({ success: false, message: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
971
backup_deploy_20250922_220227/dleV2Service.js
Normal file
971
backup_deploy_20250922_220227/dleV2Service.js
Normal file
@@ -0,0 +1,971 @@
|
|||||||
|
/**
|
||||||
|
* 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 { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const logger = require('../utils/logger');
|
||||||
|
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
||||||
|
const etherscanV2 = require('./etherscanV2VerificationService');
|
||||||
|
const verificationStore = require('./verificationStore');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сервис для управления DLE v2 (Digital Legal Entity)
|
||||||
|
* Современный подход с единым контрактом
|
||||||
|
*/
|
||||||
|
class DLEV2Service {
|
||||||
|
/**
|
||||||
|
* Создает новое DLE v2 с заданными параметрами
|
||||||
|
* @param {Object} dleParams - Параметры DLE
|
||||||
|
* @returns {Promise<Object>} - Результат создания DLE
|
||||||
|
*/
|
||||||
|
async createDLE(dleParams) {
|
||||||
|
console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!");
|
||||||
|
logger.info("🚀 DEBUG: ВХОДИМ В createDLE ФУНКЦИЮ");
|
||||||
|
let paramsFile = null;
|
||||||
|
let tempParamsFile = null;
|
||||||
|
try {
|
||||||
|
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
|
||||||
|
|
||||||
|
// Валидация входных данных
|
||||||
|
this.validateDLEParams(dleParams);
|
||||||
|
|
||||||
|
// Подготовка параметров для деплоя
|
||||||
|
const deployParams = this.prepareDeployParams(dleParams);
|
||||||
|
|
||||||
|
// Вычисляем адрес инициализатора (инициализатором является деплоер из переданного приватного ключа)
|
||||||
|
try {
|
||||||
|
const normalizedPk = dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
|
||||||
|
const initializerAddress = new ethers.Wallet(normalizedPk).address;
|
||||||
|
deployParams.initializerAddress = initializerAddress;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
|
||||||
|
const { createAndStoreNewCreate2Salt } = require('./secretStore');
|
||||||
|
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
|
||||||
|
logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`);
|
||||||
|
|
||||||
|
// Сохраняем параметры во временный файл
|
||||||
|
paramsFile = this.saveParamsToFile(deployParams);
|
||||||
|
|
||||||
|
// Копируем параметры во временный файл с предсказуемым именем
|
||||||
|
tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json');
|
||||||
|
const deployDir = path.dirname(tempParamsFile);
|
||||||
|
if (!fs.existsSync(deployDir)) {
|
||||||
|
fs.mkdirSync(deployDir, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.copyFileSync(paramsFile, tempParamsFile);
|
||||||
|
|
||||||
|
// Готовим RPC для всех выбранных сетей
|
||||||
|
const rpcUrls = [];
|
||||||
|
for (const cid of deployParams.supportedChainIds) {
|
||||||
|
logger.info(`Поиск RPC URL для chain_id: ${cid}`);
|
||||||
|
const ru = await getRpcUrlByChainId(cid);
|
||||||
|
if (!ru) {
|
||||||
|
throw new Error(`RPC URL для сети с chain_id ${cid} не найден в базе данных`);
|
||||||
|
}
|
||||||
|
rpcUrls.push(ru);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем CREATE2_SALT, RPC_URLS и initializer в файл параметров
|
||||||
|
const currentParams = JSON.parse(fs.readFileSync(tempParamsFile, 'utf8'));
|
||||||
|
// Копируем все параметры из deployParams
|
||||||
|
Object.assign(currentParams, deployParams);
|
||||||
|
currentParams.CREATE2_SALT = create2Salt;
|
||||||
|
currentParams.rpcUrls = rpcUrls;
|
||||||
|
currentParams.currentChainId = deployParams.currentChainId || deployParams.supportedChainIds[0];
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
currentParams.initializer = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000";
|
||||||
|
fs.writeFileSync(tempParamsFile, JSON.stringify(currentParams, null, 2));
|
||||||
|
|
||||||
|
logger.info(`Файл параметров скопирован и обновлен с CREATE2_SALT`);
|
||||||
|
|
||||||
|
// Лёгкая проверка баланса в первой сети
|
||||||
|
{
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrls[0]);
|
||||||
|
if (dleParams.privateKey) {
|
||||||
|
const pk = dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`;
|
||||||
|
const walletAddress = new ethers.Wallet(pk, provider).address;
|
||||||
|
const balance = await provider.getBalance(walletAddress);
|
||||||
|
|
||||||
|
const minBalance = ethers.parseEther("0.00001");
|
||||||
|
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
|
||||||
|
if (balance < minBalance) {
|
||||||
|
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dleParams.privateKey) {
|
||||||
|
throw new Error('Приватный ключ для деплоя не передан');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сначала компилируем контракты
|
||||||
|
logger.info("🔨 Компилируем контракты перед вычислением INIT_CODE_HASH...");
|
||||||
|
try {
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const compile = spawn('npx', ['hardhat', 'compile'], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
compile.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
logger.info('✅ Контракты скомпилированы успешно');
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
logger.warn(`⚠️ Компиляция завершилась с кодом: ${code}`);
|
||||||
|
resolve(); // Продолжаем даже при ошибке компиляции
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
compile.on('error', (error) => {
|
||||||
|
logger.warn('⚠️ Ошибка компиляции:', error.message);
|
||||||
|
resolve(); // Продолжаем даже при ошибке
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (compileError) {
|
||||||
|
logger.warn('⚠️ Ошибка компиляции:', compileError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// INIT_CODE_HASH будет вычислен в deploy-multichain.js
|
||||||
|
|
||||||
|
// Factory больше не используется - деплой DLE напрямую
|
||||||
|
logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`);
|
||||||
|
|
||||||
|
// Мультисетевой деплой одним вызовом
|
||||||
|
logger.info('Запуск мульти-чейн деплоя...');
|
||||||
|
logger.info("🔍 DEBUG: Подготовка к прямому деплою...");
|
||||||
|
|
||||||
|
const result = await this.runDeployMultichain(paramsFile, {
|
||||||
|
rpcUrls: rpcUrls,
|
||||||
|
chainIds: deployParams.supportedChainIds,
|
||||||
|
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
|
||||||
|
salt: create2Salt
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2));
|
||||||
|
logger.info("🔍 DEBUG: Запуск мультисетевого деплоя...");
|
||||||
|
|
||||||
|
// Сохраняем информацию о созданном DLE для отображения на странице управления
|
||||||
|
try {
|
||||||
|
logger.info('Результат деплоя для сохранения:', JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
// Проверяем структуру результата
|
||||||
|
if (!result || typeof result !== 'object') {
|
||||||
|
logger.error('Неверная структура результата деплоя:', result);
|
||||||
|
throw new Error('Неверная структура результата деплоя');
|
||||||
|
}
|
||||||
|
logger.info("🔍 DEBUG: Вызываем runDeployMultichain...");
|
||||||
|
|
||||||
|
// Если результат - массив (прямой результат из скрипта), преобразуем его
|
||||||
|
let deployResult = result;
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
logger.info('Результат - массив, преобразуем в объект');
|
||||||
|
const addresses = result.map(r => r.address);
|
||||||
|
const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase());
|
||||||
|
deployResult = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
dleAddress: addresses[0],
|
||||||
|
networks: result.map((r, index) => ({
|
||||||
|
chainId: r.chainId,
|
||||||
|
address: r.address,
|
||||||
|
success: true
|
||||||
|
})),
|
||||||
|
allSame
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstNet = Array.isArray(deployResult?.data?.networks) && deployResult.data.networks.length > 0 ? deployResult.data.networks[0] : null;
|
||||||
|
const dleData = {
|
||||||
|
name: deployParams.name,
|
||||||
|
symbol: deployParams.symbol,
|
||||||
|
location: deployParams.location,
|
||||||
|
coordinates: deployParams.coordinates,
|
||||||
|
jurisdiction: deployParams.jurisdiction,
|
||||||
|
okvedCodes: deployParams.okvedCodes || [],
|
||||||
|
kpp: deployParams.kpp,
|
||||||
|
quorumPercentage: deployParams.quorumPercentage,
|
||||||
|
initialPartners: deployParams.initialPartners || [],
|
||||||
|
initialAmounts: deployParams.initialAmounts || [],
|
||||||
|
governanceSettings: {
|
||||||
|
quorumPercentage: deployParams.quorumPercentage,
|
||||||
|
supportedChainIds: deployParams.supportedChainIds,
|
||||||
|
currentChainId: deployParams.currentChainId
|
||||||
|
},
|
||||||
|
dleAddress: (deployResult?.data?.dleAddress) || (firstNet?.address) || null,
|
||||||
|
version: 'v2',
|
||||||
|
networks: deployResult?.data?.networks || [],
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// logger.info('Данные DLE для сохранения:', JSON.stringify(dleData, null, 2)); // Убрано избыточное логирование
|
||||||
|
|
||||||
|
if (dleData.dleAddress) {
|
||||||
|
// Сохраняем данные DLE в файл
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const fileName = `dle-v2-${timestamp}.json`;
|
||||||
|
const savedPath = path.join(__dirname, '../contracts-data/dles', fileName);
|
||||||
|
|
||||||
|
// Создаем директорию, если её нет
|
||||||
|
const dlesDir = path.dirname(savedPath);
|
||||||
|
if (!fs.existsSync(dlesDir)) {
|
||||||
|
fs.mkdirSync(dlesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
|
||||||
|
// logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: dleData
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error('DLE адрес не получен после деплоя');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан
|
||||||
|
try {
|
||||||
|
if (dleParams.etherscanApiKey) {
|
||||||
|
const { setSecret } = require('./secretStore');
|
||||||
|
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Верификация выполняется в deploy-multichain.js
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при создании DLE v2:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (paramsFile || tempParamsFile) {
|
||||||
|
this.cleanupTempFiles(paramsFile, tempParamsFile);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Ошибка при очистке временных файлов (finally):', e.message);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
this.pruneOldTempFiles(24 * 60 * 60 * 1000);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Ошибка при автоочистке старых временных файлов:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидирует параметры DLE
|
||||||
|
* @param {Object} params - Параметры для валидации
|
||||||
|
*/
|
||||||
|
validateDLEParams(params) {
|
||||||
|
if (!params.name || params.name.trim() === '') {
|
||||||
|
throw new Error('Название DLE обязательно');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.symbol || params.symbol.trim() === '') {
|
||||||
|
throw new Error('Символ токена обязателен');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.location || params.location.trim() === '') {
|
||||||
|
throw new Error('Местонахождение DLE обязательно');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.initialPartners || !Array.isArray(params.initialPartners)) {
|
||||||
|
throw new Error('Партнеры должны быть массивом');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) {
|
||||||
|
throw new Error('Суммы должны быть массивом');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.initialPartners.length !== params.initialAmounts.length) {
|
||||||
|
throw new Error('Количество партнеров должно соответствовать количеству сумм распределения');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.initialPartners.length === 0) {
|
||||||
|
throw new Error('Должен быть указан хотя бы один партнер');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.quorumPercentage > 100 || params.quorumPercentage < 1) {
|
||||||
|
throw new Error('Процент кворума должен быть от 1% до 100%');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем адреса партнеров
|
||||||
|
for (let i = 0; i < params.initialPartners.length; i++) {
|
||||||
|
if (!ethers.isAddress || !ethers.isAddress(params.initialPartners[i])) {
|
||||||
|
throw new Error(`Неверный адрес партнера ${i + 1}: ${params.initialPartners[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что выбраны сети
|
||||||
|
if (!params.supportedChainIds || !Array.isArray(params.supportedChainIds) || params.supportedChainIds.length === 0) {
|
||||||
|
throw new Error('Должна быть выбрана хотя бы одна сеть для деплоя');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дополнительные проверки безопасности
|
||||||
|
if (params.name.length > 100) {
|
||||||
|
throw new Error('Название DLE слишком длинное (максимум 100 символов)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.symbol.length > 10) {
|
||||||
|
throw new Error('Символ токена слишком длинный (максимум 10 символов)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.location.length > 200) {
|
||||||
|
throw new Error('Местонахождение слишком длинное (максимум 200 символов)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем суммы токенов
|
||||||
|
for (let i = 0; i < params.initialAmounts.length; i++) {
|
||||||
|
const amount = params.initialAmounts[i];
|
||||||
|
if (typeof amount !== 'string' && typeof amount !== 'number') {
|
||||||
|
throw new Error(`Неверный тип суммы для партнера ${i + 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||||
|
if (isNaN(numAmount) || numAmount <= 0) {
|
||||||
|
throw new Error(`Неверная сумма для партнера ${i + 1}: ${amount}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем приватный ключ
|
||||||
|
if (!params.privateKey) {
|
||||||
|
throw new Error('Приватный ключ обязателен для деплоя');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pk = params.privateKey.startsWith('0x') ? params.privateKey : `0x${params.privateKey}`;
|
||||||
|
if (!/^0x[a-fA-F0-9]{64}$/.test(pk)) {
|
||||||
|
throw new Error('Неверный формат приватного ключа');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что не деплоим в mainnet без подтверждения
|
||||||
|
const mainnetChains = [1, 137, 56, 42161]; // Ethereum, Polygon, BSC, Arbitrum
|
||||||
|
const hasMainnet = params.supportedChainIds.some(id => mainnetChains.includes(id));
|
||||||
|
|
||||||
|
if (hasMainnet) {
|
||||||
|
logger.warn('⚠️ ВНИМАНИЕ: Деплой включает mainnet сети! Убедитесь, что это необходимо.');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('✅ Валидация параметров DLE пройдена успешно');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет/обновляет локальную карточку DLE для отображения в UI
|
||||||
|
* @param {Object} dleData
|
||||||
|
* @returns {string} Путь к сохраненному файлу
|
||||||
|
*/
|
||||||
|
saveDLEData(dleData) {
|
||||||
|
try {
|
||||||
|
if (!dleData || !dleData.dleAddress) {
|
||||||
|
throw new Error('Неверные данные для сохранения карточки DLE: отсутствует dleAddress');
|
||||||
|
}
|
||||||
|
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||||
|
if (!fs.existsSync(dlesDir)) {
|
||||||
|
fs.mkdirSync(dlesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если уже есть файл с таким адресом — обновим его
|
||||||
|
let targetFile = null;
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(dlesDir);
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.json') && file.includes('dle-v2-')) {
|
||||||
|
const fp = path.join(dlesDir, file);
|
||||||
|
try {
|
||||||
|
const existing = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
||||||
|
if (existing?.dleAddress && existing.dleAddress.toLowerCase() === dleData.dleAddress.toLowerCase()) {
|
||||||
|
targetFile = fp;
|
||||||
|
// Совмещаем данные (не удаляя существующие поля сетей/верификации, если присутствуют)
|
||||||
|
dleData = { ...existing, ...dleData };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
if (!targetFile) {
|
||||||
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
|
const fileName = `dle-v2-${ts}.json`;
|
||||||
|
targetFile = path.join(dlesDir, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(targetFile, JSON.stringify(dleData, null, 2));
|
||||||
|
logger.info(`Карточка DLE сохранена: ${targetFile}`);
|
||||||
|
return targetFile;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Ошибка сохранения карточки DLE:', e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подготавливает параметры для деплоя
|
||||||
|
* @param {Object} params - Параметры DLE из формы
|
||||||
|
* @returns {Object} - Подготовленные параметры для скрипта деплоя
|
||||||
|
*/
|
||||||
|
prepareDeployParams(params) {
|
||||||
|
// Создаем копию объекта, чтобы не изменять исходный
|
||||||
|
const deployParams = { ...params };
|
||||||
|
|
||||||
|
// Преобразуем суммы из строк или чисел в BigNumber, если нужно
|
||||||
|
if (deployParams.initialAmounts && Array.isArray(deployParams.initialAmounts)) {
|
||||||
|
deployParams.initialAmounts = deployParams.initialAmounts.map(rawAmount => {
|
||||||
|
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
|
||||||
|
try {
|
||||||
|
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
|
||||||
|
return ethers.parseUnits(rawAmount.toString(), 18).toString();
|
||||||
|
}
|
||||||
|
if (typeof rawAmount === 'string') {
|
||||||
|
const a = rawAmount.trim();
|
||||||
|
if (a.startsWith('0x')) {
|
||||||
|
// Уже base units (hex BigNumber) — оставляем как есть
|
||||||
|
return BigInt(a).toString();
|
||||||
|
}
|
||||||
|
// Десятичная строка — конвертируем в base units
|
||||||
|
return ethers.parseUnits(a, 18).toString();
|
||||||
|
}
|
||||||
|
// BigInt или иные типы — приводим к строке без изменения масштаба
|
||||||
|
return rawAmount.toString();
|
||||||
|
} catch (e) {
|
||||||
|
// Фолбэк: безопасно привести к строке
|
||||||
|
return String(rawAmount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убеждаемся, что okvedCodes - это массив
|
||||||
|
if (!Array.isArray(deployParams.okvedCodes)) {
|
||||||
|
deployParams.okvedCodes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем kpp в число
|
||||||
|
if (deployParams.kpp) {
|
||||||
|
deployParams.kpp = parseInt(deployParams.kpp) || 0;
|
||||||
|
} else {
|
||||||
|
deployParams.kpp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убеждаемся, что supportedChainIds - это массив
|
||||||
|
if (!Array.isArray(deployParams.supportedChainIds)) {
|
||||||
|
deployParams.supportedChainIds = [1]; // По умолчанию Ethereum
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем currentChainId как первую выбранную сеть
|
||||||
|
if (deployParams.supportedChainIds.length > 0) {
|
||||||
|
deployParams.currentChainId = deployParams.supportedChainIds[0];
|
||||||
|
} else {
|
||||||
|
deployParams.currentChainId = 1; // По умолчанию Ethereum
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем logoURI
|
||||||
|
if (deployParams.logoURI) {
|
||||||
|
// Если logoURI относительный путь, делаем его абсолютным
|
||||||
|
if (deployParams.logoURI.startsWith('/uploads/')) {
|
||||||
|
deployParams.logoURI = `http://localhost:8000${deployParams.logoURI}`;
|
||||||
|
}
|
||||||
|
// Если это placeholder, оставляем как есть
|
||||||
|
if (deployParams.logoURI.includes('placeholder.com')) {
|
||||||
|
// Оставляем как есть
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет параметры во временный файл
|
||||||
|
* @param {Object} params - Параметры для сохранения
|
||||||
|
* @returns {string} - Путь к сохраненному файлу
|
||||||
|
*/
|
||||||
|
saveParamsToFile(params) {
|
||||||
|
const tempDir = path.join(__dirname, '../temp');
|
||||||
|
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = `dle-v2-params-${Date.now()}.json`;
|
||||||
|
const filePath = path.join(tempDir, fileName);
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(params, null, 2));
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает скрипт деплоя DLE v2
|
||||||
|
* @param {string} paramsFile - Путь к файлу с параметрами
|
||||||
|
* @returns {Promise<Object>} - Результат деплоя
|
||||||
|
*/
|
||||||
|
runDeployScript(paramsFile, extraEnv = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
|
||||||
|
if (!fs.existsSync(scriptPath)) {
|
||||||
|
reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVars = {
|
||||||
|
...process.env,
|
||||||
|
RPC_URL: extraEnv.rpcUrl,
|
||||||
|
PRIVATE_KEY: extraEnv.privateKey
|
||||||
|
};
|
||||||
|
|
||||||
|
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
|
||||||
|
cwd: path.join(__dirname, '..'),
|
||||||
|
env: envVars,
|
||||||
|
stdio: ['inherit', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
hardhatProcess.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
logger.info(`[DLE v2 Deploy] ${data.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
hardhatProcess.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
logger.error(`[DLE v2 Deploy Error] ${data.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
hardhatProcess.on('close', (code) => {
|
||||||
|
try {
|
||||||
|
const result = this.extractDeployResult(stdout);
|
||||||
|
resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при извлечении результатов деплоя DLE v2:', error);
|
||||||
|
if (code === 0) {
|
||||||
|
reject(new Error('Не удалось найти информацию о созданном DLE v2'));
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Скрипт деплоя DLE v2 завершился с кодом ${code}: ${stderr}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hardhatProcess.on('error', (error) => {
|
||||||
|
logger.error('Ошибка запуска скрипта деплоя DLE v2:', error);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Мультисетевой деплой
|
||||||
|
runDeployMultichain(paramsFile, opts = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js');
|
||||||
|
if (!fs.existsSync(scriptPath)) return reject(new Error('Скрипт мультисетевого деплоя не найден: ' + scriptPath));
|
||||||
|
|
||||||
|
const envVars = {
|
||||||
|
...process.env,
|
||||||
|
PRIVATE_KEY: opts.privateKey
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = spawn('npx', ['hardhat', 'run', scriptPath], {
|
||||||
|
cwd: path.join(__dirname, '..'),
|
||||||
|
env: envVars,
|
||||||
|
stdio: ['inherit', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '', stderr = '';
|
||||||
|
p.stdout.on('data', (d) => {
|
||||||
|
stdout += d.toString();
|
||||||
|
logger.info(`[MULTICHAIN_DEPLOY] ${d.toString().trim()}`);
|
||||||
|
});
|
||||||
|
p.stderr.on('data', (d) => {
|
||||||
|
stderr += d.toString();
|
||||||
|
logger.error(`[MULTICHAIN_DEPLOY_ERR] ${d.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
p.on('close', (code) => {
|
||||||
|
try {
|
||||||
|
// Ищем результат в формате MULTICHAIN_DEPLOY_RESULT
|
||||||
|
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*\])/);
|
||||||
|
|
||||||
|
if (resultMatch) {
|
||||||
|
const deployResults = JSON.parse(resultMatch[1]);
|
||||||
|
|
||||||
|
// Преобразуем результат в нужный формат
|
||||||
|
const addresses = deployResults.map(r => r.address);
|
||||||
|
const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase());
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
dleAddress: addresses[0],
|
||||||
|
networks: deployResults.map((r, index) => ({
|
||||||
|
chainId: r.chainId,
|
||||||
|
address: r.address,
|
||||||
|
success: true
|
||||||
|
})),
|
||||||
|
allSame
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: ищем адреса DLE в выводе по новому формату
|
||||||
|
const dleAddressMatches = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/g);
|
||||||
|
if (!dleAddressMatches || dleAddressMatches.length === 0) {
|
||||||
|
throw new Error('Не найдены адреса DLE в выводе');
|
||||||
|
}
|
||||||
|
|
||||||
|
const addresses = dleAddressMatches.map(match => match.match(/(0x[a-fA-F0-9]{40})/)[1]);
|
||||||
|
const addr = addresses[0];
|
||||||
|
const allSame = addresses.every(x => x.toLowerCase() === addr.toLowerCase());
|
||||||
|
|
||||||
|
if (!allSame) {
|
||||||
|
logger.warn('Адреса отличаются между сетями — продолжаем, сохраню по-сеточно', { addresses });
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
dleAddress: addr,
|
||||||
|
networks: addresses.map((address, index) => ({
|
||||||
|
chainId: opts.chainIds[index] || index + 1,
|
||||||
|
address,
|
||||||
|
success: true
|
||||||
|
})),
|
||||||
|
allSame
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(new Error(`Ошибка мультисетевого деплоя: ${e.message}\nSTDOUT:${stdout}\nSTDERR:${stderr}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
p.on('error', (e) => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлекает результат деплоя из stdout
|
||||||
|
* @param {string} stdout - Вывод скрипта
|
||||||
|
* @returns {Object} - Результат деплоя
|
||||||
|
*/
|
||||||
|
extractDeployResult(stdout) {
|
||||||
|
// Ищем результат в формате MULTICHAIN_DEPLOY_RESULT
|
||||||
|
const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*?\])/);
|
||||||
|
|
||||||
|
if (resultMatch) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(resultMatch[1]);
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Ошибка парсинга JSON результата:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: ищем строки с адресами в выводе по новому формату
|
||||||
|
const dleAddressMatch = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/);
|
||||||
|
|
||||||
|
if (dleAddressMatch) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
dleAddress: dleAddressMatch[1],
|
||||||
|
version: 'v2'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли адрес, выводим весь stdout для отладки
|
||||||
|
console.log('Полный вывод скрипта:', stdout);
|
||||||
|
throw new Error('Не удалось извлечь адрес DLE из вывода скрипта');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает временные файлы
|
||||||
|
* @param {string} paramsFile - Путь к файлу параметров
|
||||||
|
* @param {string} tempParamsFile - Путь к временному файлу параметров
|
||||||
|
*/
|
||||||
|
cleanupTempFiles(paramsFile, tempParamsFile) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(paramsFile)) {
|
||||||
|
fs.unlinkSync(paramsFile);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(tempParamsFile)) {
|
||||||
|
fs.unlinkSync(tempParamsFile);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Не удалось очистить временные файлы:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет временные файлы параметров деплоя старше заданного возраста
|
||||||
|
* @param {number} maxAgeMs - Макс. возраст файлов в миллисекундах (по умолчанию 24ч)
|
||||||
|
*/
|
||||||
|
pruneOldTempFiles(maxAgeMs = 24 * 60 * 60 * 1000) {
|
||||||
|
const tempDir = path.join(__dirname, '../temp');
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(tempDir)) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const files = fs.readdirSync(tempDir).filter(f => f.startsWith('dle-v2-params-') && f.endsWith('.json'));
|
||||||
|
for (const f of files) {
|
||||||
|
const fp = path.join(tempDir, f);
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(fp);
|
||||||
|
if (now - st.mtimeMs > maxAgeMs) {
|
||||||
|
fs.unlinkSync(fp);
|
||||||
|
logger.info(`Удалён старый временный файл: ${fp}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Не удалось обработать файл ${fp}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Ошибка pruneOldTempFiles:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает список всех созданных DLE v2
|
||||||
|
* @returns {Array<Object>} - Список DLE v2
|
||||||
|
*/
|
||||||
|
getAllDLEs() {
|
||||||
|
try {
|
||||||
|
const dlesDir = path.join(__dirname, '../contracts-data/dles');
|
||||||
|
|
||||||
|
if (!fs.existsSync(dlesDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(dlesDir);
|
||||||
|
const allDles = files
|
||||||
|
.filter(file => file.endsWith('.json') && file.includes('dle-v2-'))
|
||||||
|
.map(file => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(fs.readFileSync(path.join(dlesDir, file), 'utf8'));
|
||||||
|
return { ...data, _fileName: file };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Ошибка при чтении файла ${file}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(dle => dle !== null);
|
||||||
|
|
||||||
|
// Группируем DLE по мультичейн деплоям
|
||||||
|
const groupedDles = this.groupMultichainDLEs(allDles);
|
||||||
|
|
||||||
|
return groupedDles;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Ошибка при получении списка DLE v2:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Группирует DLE по мультичейн деплоям
|
||||||
|
* @param {Array<Object>} allDles - Все DLE из файлов
|
||||||
|
* @returns {Array<Object>} - Сгруппированные DLE
|
||||||
|
*/
|
||||||
|
groupMultichainDLEs(allDles) {
|
||||||
|
const groups = new Map();
|
||||||
|
|
||||||
|
for (const dle of allDles) {
|
||||||
|
// Создаем ключ для группировки на основе общих параметров
|
||||||
|
const groupKey = this.createGroupKey(dle);
|
||||||
|
|
||||||
|
if (!groups.has(groupKey)) {
|
||||||
|
groups.set(groupKey, {
|
||||||
|
// Основные данные из первого DLE
|
||||||
|
name: dle.name,
|
||||||
|
symbol: dle.symbol,
|
||||||
|
location: dle.location,
|
||||||
|
coordinates: dle.coordinates,
|
||||||
|
jurisdiction: dle.jurisdiction,
|
||||||
|
oktmo: dle.oktmo,
|
||||||
|
okvedCodes: dle.okvedCodes,
|
||||||
|
kpp: dle.kpp,
|
||||||
|
quorumPercentage: dle.quorumPercentage,
|
||||||
|
version: dle.version || 'v2',
|
||||||
|
deployedMultichain: true,
|
||||||
|
// Мультичейн информация
|
||||||
|
networks: [],
|
||||||
|
// Модули (одинаковые во всех сетях)
|
||||||
|
modules: dle.modules,
|
||||||
|
// Время создания (самое раннее)
|
||||||
|
creationTimestamp: dle.creationTimestamp,
|
||||||
|
creationBlock: dle.creationBlock
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groups.get(groupKey);
|
||||||
|
|
||||||
|
// Если у DLE есть массив networks, используем его
|
||||||
|
if (dle.networks && Array.isArray(dle.networks)) {
|
||||||
|
for (const network of dle.networks) {
|
||||||
|
group.networks.push({
|
||||||
|
chainId: network.chainId,
|
||||||
|
dleAddress: network.address || network.dleAddress,
|
||||||
|
factoryAddress: network.factoryAddress,
|
||||||
|
rpcUrl: network.rpcUrl || this.getRpcUrlForChain(network.chainId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Старый формат: добавляем информацию о сети из корня DLE
|
||||||
|
group.networks.push({
|
||||||
|
chainId: dle.chainId,
|
||||||
|
dleAddress: dle.dleAddress,
|
||||||
|
factoryAddress: dle.factoryAddress,
|
||||||
|
rpcUrl: dle.rpcUrl || this.getRpcUrlForChain(dle.chainId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем время создания на самое раннее
|
||||||
|
if (dle.creationTimestamp && (!group.creationTimestamp || dle.creationTimestamp < group.creationTimestamp)) {
|
||||||
|
group.creationTimestamp = dle.creationTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем группы в массив
|
||||||
|
return Array.from(groups.values()).map(group => ({
|
||||||
|
...group,
|
||||||
|
// Основной адрес DLE (из первой сети)
|
||||||
|
dleAddress: group.networks[0]?.dleAddress,
|
||||||
|
// Общее количество сетей
|
||||||
|
totalNetworks: group.networks.length,
|
||||||
|
// Поддерживаемые сети
|
||||||
|
supportedChainIds: group.networks.map(n => n.chainId)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает ключ для группировки DLE
|
||||||
|
* @param {Object} dle - Данные DLE
|
||||||
|
* @returns {string} - Ключ группировки
|
||||||
|
*/
|
||||||
|
createGroupKey(dle) {
|
||||||
|
// Группируем по основным параметрам DLE
|
||||||
|
const keyParts = [
|
||||||
|
dle.name,
|
||||||
|
dle.symbol,
|
||||||
|
dle.location,
|
||||||
|
dle.coordinates,
|
||||||
|
dle.jurisdiction,
|
||||||
|
dle.oktmo,
|
||||||
|
dle.kpp,
|
||||||
|
dle.quorumPercentage,
|
||||||
|
// Сортируем okvedCodes для стабильного ключа
|
||||||
|
Array.isArray(dle.okvedCodes) ? dle.okvedCodes.sort().join(',') : '',
|
||||||
|
// Сортируем supportedChainIds для стабильного ключа
|
||||||
|
Array.isArray(dle.supportedChainIds) ? dle.supportedChainIds.sort().join(',') : ''
|
||||||
|
];
|
||||||
|
|
||||||
|
return keyParts.join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает RPC URL для сети
|
||||||
|
* @param {number} chainId - ID сети
|
||||||
|
* @returns {string|null} - RPC URL
|
||||||
|
*/
|
||||||
|
getRpcUrlForChain(chainId) {
|
||||||
|
try {
|
||||||
|
// Простая маппинг для основных сетей
|
||||||
|
const rpcMap = {
|
||||||
|
1: 'https://eth-mainnet.g.alchemy.com/v2/demo',
|
||||||
|
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 rpcMap[chainId] || null;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет балансы в указанных сетях
|
||||||
|
* @param {number[]} chainIds - Массив chainId для проверки
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @returns {Promise<Object>} - Результат проверки балансов
|
||||||
|
*/
|
||||||
|
async checkBalances(chainIds, privateKey) {
|
||||||
|
const { getRpcUrlByChainId } = require('./rpcProviderService');
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
const balances = [];
|
||||||
|
const insufficient = [];
|
||||||
|
|
||||||
|
for (const chainId of chainIds) {
|
||||||
|
try {
|
||||||
|
const rpcUrl = await getRpcUrlByChainId(chainId);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
balances.push({
|
||||||
|
chainId,
|
||||||
|
balanceEth: '0',
|
||||||
|
ok: false,
|
||||||
|
error: 'RPC URL не найден'
|
||||||
|
});
|
||||||
|
insufficient.push(chainId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
const wallet = new ethers.Wallet(privateKey, provider);
|
||||||
|
const balance = await provider.getBalance(wallet.address);
|
||||||
|
|
||||||
|
const balanceEth = ethers.formatEther(balance);
|
||||||
|
const minBalance = ethers.parseEther("0.001");
|
||||||
|
const ok = balance >= minBalance;
|
||||||
|
|
||||||
|
balances.push({
|
||||||
|
chainId,
|
||||||
|
address: wallet.address,
|
||||||
|
balanceEth,
|
||||||
|
ok
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
insufficient.push(chainId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
balances.push({
|
||||||
|
chainId,
|
||||||
|
balanceEth: '0',
|
||||||
|
ok: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
insufficient.push(chainId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
balances,
|
||||||
|
insufficient,
|
||||||
|
allSufficient: insufficient.length === 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new DLEV2Service();
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
|
||||||
# Архитектура проекта DLE
|
# Архитектура проекта DLE
|
||||||
|
|
||||||
## 🎯 Общий принцип
|
## 🎯 Общий принцип
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
|
||||||
# API Endpoints для обновленного смарт контракта DLE
|
# API Endpoints для обновленного смарт контракта DLE
|
||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
- Делегирование «только на себя»: 1 токен = 1 голос, запрет делегирования третьим лицам.
|
- Делегирование «только на себя»: 1 токен = 1 голос, запрет делегирования третьим лицам.
|
||||||
- Модульность: казна, таймлок, деактивация, коммуникации выделены в отдельные модули, операции выполняются через ядро DLE.
|
- Модульность: казна, таймлок, деактивация, коммуникации выделены в отдельные модули, операции выполняются через ядро DLE.
|
||||||
- «100% или ничего»: много-сетевые операции исполняются только при готовности всех целевых сетей.
|
- «100% или ничего»: много-сетевые операции исполняются только при готовности всех целевых сетей.
|
||||||
- Детерминированный деплой: `FactoryDeployer` + CREATE2 для одинаковых адресов во всех выбранных сетях; INIT_CODE_HASH рассчитывается автоматически из актуального initCode.
|
- Детерминированный деплой: CREATE с выровненным nonce для одинаковых адресов во всех выбранных сетях.
|
||||||
- Аналитика: добавлены view‑функции для сводок, пагинации и агрегирования по предложениям.
|
- Аналитика: добавлены view‑функции для сводок, пагинации и агрегирования по предложениям.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
|
||||||
# Руководство по деплою DLE v2
|
# Руководство по деплою DLE v2
|
||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
@@ -9,7 +21,7 @@ DLE v2 (Digital Legal Entity) - это система для создания ц
|
|||||||
### Компоненты системы
|
### Компоненты системы
|
||||||
|
|
||||||
1. **DLE.sol** - Основной смарт-контракт с ERC-20 токенами управления
|
1. **DLE.sol** - Основной смарт-контракт с ERC-20 токенами управления
|
||||||
2. **FactoryDeployer.sol** - Фабрика для детерминистического деплоя через CREATE2
|
2. **Детерминированный деплой** - через CREATE с выровненным nonce для одинаковых адресов
|
||||||
3. **Модули** - Дополнительная функциональность (Treasury, Timelock, etc.)
|
3. **Модули** - Дополнительная функциональность (Treasury, Timelock, etc.)
|
||||||
|
|
||||||
### Мульти-чейн поддержка
|
### Мульти-чейн поддержка
|
||||||
@@ -60,10 +72,9 @@ DLE v2 (Digital Legal Entity) - это система для создания ц
|
|||||||
1. **Проверяет балансы** во всех выбранных сетях
|
1. **Проверяет балансы** во всех выбранных сетях
|
||||||
2. **Компилирует контракты** через Hardhat
|
2. **Компилирует контракты** через Hardhat
|
||||||
3. **Проверяет Factory адреса** в базе данных
|
3. **Проверяет Factory адреса** в базе данных
|
||||||
4. **Деплоит FactoryDeployer** (если не найден) с одинаковым адресом
|
4. **Выравнивает nonce** для детерминированного деплоя
|
||||||
5. **Сохраняет Factory адреса** в базу данных для переиспользования
|
5. **Вычисляет адрес DLE** через CREATE с выровненным nonce
|
||||||
6. **Создает CREATE2 salt** на основе параметров DLE
|
6. **Деплоит DLE** с одинаковым адресом во всех сетях
|
||||||
7. **Деплоит DLE** через FactoryDeployer с одинаковым адресом
|
|
||||||
8. **Деплоит базовые модули** (Treasury, Timelock, Reader) в каждой сети
|
8. **Деплоит базовые модули** (Treasury, Timelock, Reader) в каждой сети
|
||||||
9. **Инициализирует модули** в DLE контракте
|
9. **Инициализирует модули** в DLE контракте
|
||||||
10. **Верифицирует контракты** в Etherscan (опционально)
|
10. **Верифицирует контракты** в Etherscan (опционально)
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
|
||||||
# Архитектура фронтенда DLE
|
# Архитектура фронтенда DLE
|
||||||
|
|
||||||
## 📁 Структура сервисов
|
## 📁 Структура сервисов
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
|
||||||
# Архитектура модулей DLE
|
# Архитектура модулей DLE
|
||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
|
|||||||
628
docs/MODULE_DEPLOYMENT_IMPROVEMENTS.md
Normal file
628
docs/MODULE_DEPLOYMENT_IMPROVEMENTS.md
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
<!--
|
||||||
|
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
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Улучшения системы деплоя и управления модулями DLE
|
||||||
|
|
||||||
|
## Описание задачи
|
||||||
|
|
||||||
|
Пользователь хочет улучшить процесс деплоя и управления модулями DLE, чтобы обеспечить автоматическую инициализацию и верификацию модулей во всех выбранных пользователем блокчейн-сетях.
|
||||||
|
|
||||||
|
## Текущая ситуация
|
||||||
|
|
||||||
|
### Что уже работает:
|
||||||
|
- ✅ Деплой основного DLE контракта в 4 сетях с одинаковым адресом (через CREATE2)
|
||||||
|
- ✅ Деплой модулей (Treasury, Timelock, Reader) в каждой сети
|
||||||
|
- ✅ Автоматическая инициализация базовых модулей через `initializeBaseModules()`
|
||||||
|
- ✅ Верификация контрактов в каждой сети
|
||||||
|
- ✅ Отображение модулей в виде карточек с адресами во всех сетях
|
||||||
|
|
||||||
|
### Проблемы:
|
||||||
|
- ❌ Если деплой модулей в одной сети падает, инициализация не происходит
|
||||||
|
- ❌ Нет механизма повторной инициализации модулей
|
||||||
|
- ❌ Нет проверки статуса инициализации перед деплоем
|
||||||
|
- ❌ Верификация может падать из-за таймаутов блокчейн-эксплореров
|
||||||
|
- ❌ Нет удобного интерфейса для управления модулями
|
||||||
|
|
||||||
|
## Требования пользователя
|
||||||
|
|
||||||
|
### 1. Workflow деплоя
|
||||||
|
**Цель:** При заполнении формы и нажатии на кнопку "Деплой" пользователь должен получить:
|
||||||
|
- Основной смарт-контракт DLE
|
||||||
|
- 3 модуля (Treasury, Timelock, Reader)
|
||||||
|
- Модули должны быть **сразу инициализированы** во всех выбранных сетях
|
||||||
|
- Модули должны быть **сразу верифицированы** во всех выбранных сетях
|
||||||
|
|
||||||
|
### 2. Отображение модулей
|
||||||
|
**Текущее состояние:** ✅ Уже реализовано
|
||||||
|
- Одна карточка для каждого модуля
|
||||||
|
- В карточке показаны адреса модуля во всех сетях
|
||||||
|
- Статус верификации для каждой сети
|
||||||
|
- Кнопка "Настроить" для перехода к настройкам модуля
|
||||||
|
|
||||||
|
### 3. Управление модулями
|
||||||
|
**Требования:**
|
||||||
|
- Кнопка "Настроить" должна открывать страницу с блоками для настройки модулей
|
||||||
|
- Возможность управления модулями через веб-интерфейс
|
||||||
|
- Отображение статуса инициализации и верификации
|
||||||
|
|
||||||
|
## Реализованные улучшения
|
||||||
|
|
||||||
|
### 1. Backend API Endpoints
|
||||||
|
|
||||||
|
#### `/api/dle-modules/initialize-modules-all-networks`
|
||||||
|
**Назначение:** Автоматическая инициализация всех модулей во всех поддерживаемых сетях
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функциональность:**
|
||||||
|
- Получает список поддерживаемых сетей из DLE контракта
|
||||||
|
- Проверяет статус инициализации в каждой сети
|
||||||
|
- Если модули не инициализированы, вызывает `initializeBaseModules()`
|
||||||
|
- Возвращает детальный отчет по каждой сети
|
||||||
|
|
||||||
|
**Возвращаемые статусы:**
|
||||||
|
- `success` - модули успешно инициализированы
|
||||||
|
- `already_initialized` - модули уже инициализированы
|
||||||
|
- `modules_not_deployed` - не все модули задеплоены
|
||||||
|
- `error` - ошибка инициализации
|
||||||
|
|
||||||
|
#### `/api/dle-modules/verify-modules-all-networks`
|
||||||
|
**Назначение:** Автоматическая верификация всех модулей во всех поддерживаемых сетях
|
||||||
|
|
||||||
|
**Параметры:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Функциональность:**
|
||||||
|
- Получает адреса всех модулей в каждой сети
|
||||||
|
- Отправляет запросы на верификацию в Etherscan/блокчейн-эксплореры
|
||||||
|
- Использует стандартный JSON input для верификации
|
||||||
|
- Возвращает детальный отчет по каждому модулю в каждой сети
|
||||||
|
|
||||||
|
**Возвращаемые статусы:**
|
||||||
|
- `success` - модуль успешно верифицирован
|
||||||
|
- `failed` - ошибка верификации
|
||||||
|
- `not_deployed` - модуль не задеплоен
|
||||||
|
- `error` - ошибка процесса верификации
|
||||||
|
|
||||||
|
### 2. Frontend Service Functions
|
||||||
|
|
||||||
|
#### `initializeModulesAllNetworks(dleAddress, privateKey)`
|
||||||
|
**Назначение:** Вызов API для инициализации модулей
|
||||||
|
|
||||||
|
#### `verifyModulesAllNetworks(dleAddress, privateKey)`
|
||||||
|
**Назначение:** Вызов API для верификации модулей
|
||||||
|
|
||||||
|
### 3. Улучшенный интерфейс модулей
|
||||||
|
|
||||||
|
#### Обновленная карточка модуля:
|
||||||
|
- **Основные действия:** Кнопка "Настроить" (приоритетная)
|
||||||
|
- **Дополнительные действия:** Удалить/Активировать модуль
|
||||||
|
- **Верификация:** Отдельные кнопки для каждой сети
|
||||||
|
|
||||||
|
#### Новые стили:
|
||||||
|
- Группировка кнопок по функциональности
|
||||||
|
- Улучшенная компоновка элементов
|
||||||
|
- Адаптивный дизайн для разных размеров экрана
|
||||||
|
|
||||||
|
## Предлагаемый Workflow (Поэтапный подход с повторами)
|
||||||
|
|
||||||
|
### Этап 1: Деплой основного DLE контракта
|
||||||
|
```bash
|
||||||
|
# Деплой только DLE контракта во всех выбранных сетях
|
||||||
|
npx hardhat run scripts/deploy/deploy-dle-only.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 2: Проверка успеха деплоя DLE (с повторами)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/check-dle-deployment-status
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"chainIds": [11155111, 17000, 421614, 84532],
|
||||||
|
"maxRetries": 5,
|
||||||
|
"retryDelay": 30000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Логика повторов:**
|
||||||
|
- Если не все сети успешны → ждем 30 сек → повторяем проверку
|
||||||
|
- Максимум 5 попыток
|
||||||
|
- Если после 5 попыток не все сети готовы → ошибка
|
||||||
|
|
||||||
|
### Этап 3: Верификация DLE контракта (с повторами)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/verify-dle-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"privateKey": "0x...",
|
||||||
|
"maxRetries": 3,
|
||||||
|
"retryDelay": 60000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Логика повторов:**
|
||||||
|
- Если верификация не удалась → ждем 60 сек → повторяем
|
||||||
|
- Максимум 3 попытки
|
||||||
|
- Etherscan может быть перегружен, поэтому больше времени между попытками
|
||||||
|
|
||||||
|
### Этап 4: Деплой модуля 1 (TreasuryModule) (с повторами)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/deploy-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "treasury",
|
||||||
|
"privateKey": "0x...",
|
||||||
|
"maxRetries": 3,
|
||||||
|
"retryDelay": 45000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Логика повторов:**
|
||||||
|
- Если деплой в какой-то сети упал → ждем 45 сек → повторяем только для неудачных сетей
|
||||||
|
- Максимум 3 попытки
|
||||||
|
- Gas price может быть высоким, поэтому больше времени между попытками
|
||||||
|
|
||||||
|
### Этап 5: Проверка успеха деплоя TreasuryModule (с повторами)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/check-module-deployment-status
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "treasury",
|
||||||
|
"chainIds": [11155111, 17000, 421614, 84532],
|
||||||
|
"maxRetries": 5,
|
||||||
|
"retryDelay": 30000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 6: Верификация TreasuryModule (с повторами)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/verify-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "treasury",
|
||||||
|
"privateKey": "0x...",
|
||||||
|
"maxRetries": 3,
|
||||||
|
"retryDelay": 60000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 7: Инициализация TreasuryModule (с повторами)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/initialize-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "treasury",
|
||||||
|
"privateKey": "0x...",
|
||||||
|
"maxRetries": 3,
|
||||||
|
"retryDelay": 30000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Логика повторов:**
|
||||||
|
- Если инициализация упала → ждем 30 сек → повторяем
|
||||||
|
- Максимум 3 попытки
|
||||||
|
- Network congestion может влиять на транзакции
|
||||||
|
|
||||||
|
### Этап 8: Деплой модуля 2 (TimelockModule)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/deploy-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "timelock",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 9: Проверка успеха деплоя TimelockModule
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/check-module-deployment-status
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "timelock",
|
||||||
|
"chainIds": [11155111, 17000, 421614, 84532]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 10: Верификация TimelockModule
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/verify-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "timelock",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 11: Инициализация TimelockModule
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/initialize-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "timelock",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 12: Деплой модуля 3 (DLEReader)
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/deploy-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "reader",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 13: Проверка успеха деплоя DLEReader
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/check-module-deployment-status
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "reader",
|
||||||
|
"chainIds": [11155111, 17000, 421614, 84532]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 14: Верификация DLEReader
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/verify-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "reader",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 15: Инициализация DLEReader
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/initialize-module-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"moduleType": "reader",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 16: Финальная инициализация всех модулей
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/initialize-base-modules-all-networks
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"privateKey": "0x..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Этап 17: Финальная проверка и отображение
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/final-deployment-check
|
||||||
|
{
|
||||||
|
"dleAddress": "0x...",
|
||||||
|
"chainIds": [11155111, 17000, 421614, 84532]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**Логика финальной проверки:**
|
||||||
|
- Проверяем, что DLE задеплоен во всех сетях
|
||||||
|
- Проверяем, что все модули задеплоены во всех сетях
|
||||||
|
- Проверяем, что все модули верифицированы во всех сетях
|
||||||
|
- Проверяем, что все модули инициализированы во всех сетях
|
||||||
|
|
||||||
|
**Только если ВСЕ проверки пройдены:**
|
||||||
|
- ✅ Карточки DLE и модулей появляются в интерфейсе
|
||||||
|
- ✅ Пользователь может управлять модулями
|
||||||
|
- ✅ Все функции доступны
|
||||||
|
|
||||||
|
**Если хотя бы одна проверка не пройдена:**
|
||||||
|
- ❌ Карточки НЕ отображаются
|
||||||
|
- ❌ Показывается статус "Деплой в процессе" или "Деплой не завершен"
|
||||||
|
- ❌ Предлагается продолжить деплой или исправить ошибки
|
||||||
|
|
||||||
|
## Логика отображения интерфейса
|
||||||
|
|
||||||
|
### Состояния деплоя:
|
||||||
|
|
||||||
|
#### 1. **Деплой не начат**
|
||||||
|
```javascript
|
||||||
|
// Интерфейс показывает:
|
||||||
|
- Форму деплоя DLE
|
||||||
|
- Кнопку "Начать деплой"
|
||||||
|
- Нет карточек модулей
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. **Деплой в процессе**
|
||||||
|
```javascript
|
||||||
|
// Интерфейс показывает:
|
||||||
|
- Прогресс-бар с текущим этапом
|
||||||
|
- Логи выполнения в реальном времени
|
||||||
|
- Кнопку "Остановить деплой" (опционально)
|
||||||
|
- Нет карточек модулей
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. **Деплой частично завершен (ошибка)**
|
||||||
|
```javascript
|
||||||
|
// Интерфейс показывает:
|
||||||
|
- Статус "Деплой не завершен"
|
||||||
|
- Список успешных этапов
|
||||||
|
- Список неудачных этапов с ошибками
|
||||||
|
- Кнопки "Продолжить деплой" или "Начать заново"
|
||||||
|
- Нет карточек модулей
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. **Деплой полностью завершен**
|
||||||
|
```javascript
|
||||||
|
// Интерфейс показывает:
|
||||||
|
- ✅ Карточки DLE и всех модулей
|
||||||
|
- ✅ Все функции управления доступны
|
||||||
|
- ✅ Кнопки "Настроить" для каждого модуля
|
||||||
|
- ✅ Статус "Деплой успешно завершен"
|
||||||
|
```
|
||||||
|
|
||||||
|
### API для проверки статуса деплоя:
|
||||||
|
```javascript
|
||||||
|
POST /api/dle-modules/get-deployment-status
|
||||||
|
{
|
||||||
|
"dleAddress": "0x..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращает:
|
||||||
|
{
|
||||||
|
"status": "completed|in_progress|failed|not_started",
|
||||||
|
"currentStage": "deploy_dle|verify_dle|deploy_treasury|...",
|
||||||
|
"completedStages": ["deploy_dle", "verify_dle"],
|
||||||
|
"failedStages": [],
|
||||||
|
"progress": 85, // процент завершения
|
||||||
|
"canShowCards": false, // только true если status === "completed"
|
||||||
|
"errors": [],
|
||||||
|
"nextAction": "continue_deployment|restart_deployment|none"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Логика повторов
|
||||||
|
|
||||||
|
### Общие принципы:
|
||||||
|
1. **Каждый этап повторяется до успеха** или до исчерпания попыток
|
||||||
|
2. **Разные задержки** для разных типов операций:
|
||||||
|
- Проверки: 30 сек (быстрые операции)
|
||||||
|
- Деплой: 45 сек (gas price может измениться)
|
||||||
|
- Верификация: 60 сек (Etherscan может быть перегружен)
|
||||||
|
- Инициализация: 30 сек (network congestion)
|
||||||
|
|
||||||
|
3. **Умные повторы:**
|
||||||
|
- Если операция частично успешна → повторяем только для неудачных сетей
|
||||||
|
- Если операция полностью провалилась → повторяем для всех сетей
|
||||||
|
- Логируем каждую попытку для диагностики
|
||||||
|
|
||||||
|
4. **Критические ошибки:**
|
||||||
|
- Если после всех попыток операция не удалась → останавливаем весь процесс
|
||||||
|
- Показываем детальный отчет о том, что не удалось
|
||||||
|
- Предлагаем варианты решения (повторить, пропустить, откатиться)
|
||||||
|
|
||||||
|
### Пример логики повторов:
|
||||||
|
```javascript
|
||||||
|
async function executeWithRetries(operation, maxRetries, retryDelay) {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await operation();
|
||||||
|
|
||||||
|
// Проверяем, все ли сети успешны
|
||||||
|
const failedNetworks = result.filter(r => r.status !== 'success');
|
||||||
|
|
||||||
|
if (failedNetworks.length === 0) {
|
||||||
|
console.log(`✅ Операция успешна с попытки ${attempt}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
console.log(`⚠️ Попытка ${attempt} частично успешна. Повторяем через ${retryDelay}мс...`);
|
||||||
|
console.log(`Неудачные сети: ${failedNetworks.map(n => n.networkName).join(', ')}`);
|
||||||
|
await sleep(retryDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
console.log(`❌ Попытка ${attempt} провалилась: ${error.message}`);
|
||||||
|
console.log(`Повторяем через ${retryDelay}мс...`);
|
||||||
|
await sleep(retryDelay);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Операция провалилась после ${maxRetries} попыток: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Преимущества поэтапного подхода
|
||||||
|
|
||||||
|
### 1. Максимальная надежность
|
||||||
|
- ✅ **Проверка на каждом этапе** - если что-то пошло не так, процесс останавливается
|
||||||
|
- ✅ **Изоляция ошибок** - проблема с одним модулем не влияет на другие
|
||||||
|
- ✅ **Возможность восстановления** - можно продолжить с места остановки
|
||||||
|
- ✅ **Детальная диагностика** - точно знаем, на каком этапе произошла ошибка
|
||||||
|
- ✅ **Автоматические повторы** - временные проблемы решаются автоматически
|
||||||
|
- ✅ **Устойчивость к сбоям** - network congestion, gas spikes, Etherscan overload
|
||||||
|
|
||||||
|
### 2. Гибкость управления
|
||||||
|
- ✅ **Выборочный деплой** - можно деплоить только нужные модули
|
||||||
|
- ✅ **Повторные попытки** - можно повторить только неудачный этап
|
||||||
|
- ✅ **Параллельная работа** - разные модули можно деплоить независимо
|
||||||
|
- ✅ **Контроль качества** - верификация после каждого деплоя
|
||||||
|
|
||||||
|
### 3. Улучшенная диагностика
|
||||||
|
- ✅ **Пошаговые логи** - детальная информация о каждом этапе
|
||||||
|
- ✅ **Статусы в реальном времени** - видно прогресс выполнения
|
||||||
|
- ✅ **Обработка ошибок** - понятные сообщения об ошибках
|
||||||
|
- ✅ **История операций** - можно отследить все выполненные действия
|
||||||
|
|
||||||
|
### 4. Масштабируемость
|
||||||
|
- ✅ **Легко добавить новые модули** - просто добавить новые этапы
|
||||||
|
- ✅ **Поддержка новых сетей** - автоматически работает для всех сетей
|
||||||
|
- ✅ **Модульная архитектура** - каждый endpoint независим
|
||||||
|
- ✅ **Расширяемость** - легко добавить новые типы операций
|
||||||
|
|
||||||
|
### 5. Безопасность
|
||||||
|
- ✅ **Постепенное развертывание** - минимизация рисков
|
||||||
|
- ✅ **Проверка перед выполнением** - валидация на каждом этапе
|
||||||
|
- ✅ **Откат изменений** - можно отменить неудачные операции
|
||||||
|
- ✅ **Аудит операций** - полная история всех действий
|
||||||
|
|
||||||
|
## Пример использования поэтапного подхода
|
||||||
|
|
||||||
|
### Сценарий: Деплой DLE с модулями в 4 сетях
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Деплой DLE контракта
|
||||||
|
const dleResult = await deployDLE({
|
||||||
|
networks: [11155111, 17000, 421614, 84532],
|
||||||
|
privateKey: "0x...",
|
||||||
|
params: { name: "My DLE", symbol: "MDLE" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Проверка деплоя DLE
|
||||||
|
const dleStatus = await checkDLEStatus(dleResult.address, [11155111, 17000, 421614, 84532]);
|
||||||
|
if (!dleStatus.allDeployed) {
|
||||||
|
throw new Error("DLE не задеплоен во всех сетях");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Верификация DLE
|
||||||
|
const dleVerification = await verifyDLE(dleResult.address, "0x...");
|
||||||
|
console.log("DLE верификация:", dleVerification);
|
||||||
|
|
||||||
|
// 4. Деплой TreasuryModule
|
||||||
|
const treasuryResult = await deployModule("treasury", dleResult.address, "0x...");
|
||||||
|
|
||||||
|
// 5. Проверка деплоя TreasuryModule
|
||||||
|
const treasuryStatus = await checkModuleStatus("treasury", dleResult.address, [11155111, 17000, 421614, 84532]);
|
||||||
|
if (!treasuryStatus.allDeployed) {
|
||||||
|
throw new Error("TreasuryModule не задеплоен во всех сетях");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Верификация TreasuryModule
|
||||||
|
const treasuryVerification = await verifyModule("treasury", dleResult.address, "0x...");
|
||||||
|
console.log("TreasuryModule верификация:", treasuryVerification);
|
||||||
|
|
||||||
|
// 7. Инициализация TreasuryModule
|
||||||
|
const treasuryInit = await initializeModule("treasury", dleResult.address, "0x...");
|
||||||
|
console.log("TreasuryModule инициализация:", treasuryInit);
|
||||||
|
|
||||||
|
// 8-15. Повторяем для TimelockModule и DLEReader...
|
||||||
|
|
||||||
|
// 16. Финальная инициализация всех модулей
|
||||||
|
const finalInit = await initializeBaseModules(dleResult.address, "0x...");
|
||||||
|
console.log("Финальная инициализация:", finalInit);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Обработка ошибок
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
// Деплой модуля
|
||||||
|
const result = await deployModule("treasury", dleAddress, privateKey);
|
||||||
|
|
||||||
|
// Проверка успеха
|
||||||
|
const status = await checkModuleStatus("treasury", dleAddress, chainIds);
|
||||||
|
|
||||||
|
if (status.errors.length > 0) {
|
||||||
|
console.log("Ошибки деплоя:", status.errors);
|
||||||
|
// Можно повторить только для сетей с ошибками
|
||||||
|
const retryResult = await deployModule("treasury", dleAddress, privateKey, status.errorChains);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Критическая ошибка:", error);
|
||||||
|
// Логирование и уведомление пользователя
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Следующие шаги
|
||||||
|
|
||||||
|
### 1. Реализация новых API endpoints
|
||||||
|
- `check-dle-deployment-status` - проверка деплоя DLE
|
||||||
|
- `check-module-deployment-status` - проверка деплоя модуля
|
||||||
|
- `deploy-module-all-networks` - деплой одного модуля
|
||||||
|
- `verify-dle-all-networks` - верификация DLE
|
||||||
|
- `verify-module-all-networks` - верификация модуля
|
||||||
|
- `initialize-module-all-networks` - инициализация модуля
|
||||||
|
- `initialize-base-modules-all-networks` - финальная инициализация
|
||||||
|
|
||||||
|
### 2. Веб-интерфейс для поэтапного деплоя
|
||||||
|
- Мастер деплоя с пошаговым интерфейсом
|
||||||
|
- Прогресс-бар для каждого этапа
|
||||||
|
- Обработка ошибок и повторные попытки
|
||||||
|
- Логи операций в реальном времени
|
||||||
|
|
||||||
|
### 3. Интеграция с существующей формой деплоя
|
||||||
|
- Добавить опцию "Поэтапный деплой"
|
||||||
|
- Автоматическое выполнение всех этапов
|
||||||
|
- Уведомления о статусе каждого этапа
|
||||||
|
|
||||||
|
### 4. Мониторинг и логирование
|
||||||
|
- Детальные логи всех операций
|
||||||
|
- История деплоев и их статусов
|
||||||
|
- Алерты при ошибках
|
||||||
|
- Метрики производительности
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
### Поддерживаемые сети:
|
||||||
|
- Sepolia (Chain ID: 11155111)
|
||||||
|
- Holesky (Chain ID: 17000)
|
||||||
|
- Arbitrum Sepolia (Chain ID: 421614)
|
||||||
|
- Base Sepolia (Chain ID: 84532)
|
||||||
|
|
||||||
|
### Модули:
|
||||||
|
- **TreasuryModule** - управление финансами
|
||||||
|
- **TimelockModule** - задержки исполнения
|
||||||
|
- **DLEReader** - чтение данных DLE
|
||||||
|
|
||||||
|
### API Endpoints:
|
||||||
|
|
||||||
|
#### Основные endpoints (уже реализованы):
|
||||||
|
- `POST /api/dle-modules/initialize-modules-all-networks` - инициализация всех модулей
|
||||||
|
- `POST /api/dle-modules/verify-modules-all-networks` - верификация всех модулей
|
||||||
|
- `POST /api/dle-modules/get-all-modules` - получение списка модулей
|
||||||
|
- `POST /api/dle-modules/get-networks-info` - информация о сетях
|
||||||
|
|
||||||
|
#### Новые endpoints (требуют реализации):
|
||||||
|
|
||||||
|
**Проверка статуса деплоя:**
|
||||||
|
- `POST /api/dle-modules/check-dle-deployment-status` - проверка деплоя DLE контракта
|
||||||
|
- `POST /api/dle-modules/check-module-deployment-status` - проверка деплоя конкретного модуля
|
||||||
|
|
||||||
|
**Деплой модулей:**
|
||||||
|
- `POST /api/dle-modules/deploy-module-all-networks` - деплой одного модуля во всех сетях
|
||||||
|
|
||||||
|
**Верификация:**
|
||||||
|
- `POST /api/dle-modules/verify-dle-all-networks` - верификация DLE контракта
|
||||||
|
- `POST /api/dle-modules/verify-module-all-networks` - верификация одного модуля
|
||||||
|
|
||||||
|
**Инициализация:**
|
||||||
|
- `POST /api/dle-modules/initialize-module-all-networks` - инициализация одного модуля
|
||||||
|
- `POST /api/dle-modules/initialize-base-modules-all-networks` - финальная инициализация всех модулей
|
||||||
|
|
||||||
|
**Управление отображением:**
|
||||||
|
- `POST /api/dle-modules/final-deployment-check` - финальная проверка готовности
|
||||||
|
- `POST /api/dle-modules/get-deployment-status` - получение статуса деплоя
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
Реализованные улучшения обеспечивают:
|
||||||
|
1. **Полную автоматизацию** процесса деплоя модулей
|
||||||
|
2. **Надежность** через проверки статуса и обработку ошибок
|
||||||
|
3. **Удобство использования** через улучшенный интерфейс
|
||||||
|
4. **Масштабируемость** для добавления новых сетей и модулей
|
||||||
|
|
||||||
|
Пользователь теперь может одним кликом развернуть полностью функциональный DLE с инициализированными и верифицированными модулями во всех выбранных сетях.
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
- Multi‑Chain исполнение: выполнение в целевых сетях по EIP‑712 подписям холдеров, проверяется суммарная голосующая сила на зафиксированном `timepoint` (без доверия к мостам).
|
- Multi‑Chain исполнение: выполнение в целевых сетях по EIP‑712 подписям холдеров, проверяется суммарная голосующая сила на зафиксированном `timepoint` (без доверия к мостам).
|
||||||
- «100% или ничего»: операции считаются успешными только при готовности/успешности всех целевых сетей.
|
- «100% или ничего»: операции считаются успешными только при готовности/успешности всех целевых сетей.
|
||||||
- Модули вынесены отдельно: `Treasury`, `Timelock`, `Deactivation`, `Communication` и др. Управление только через предложения.
|
- Модули вынесены отдельно: `Treasury`, `Timelock`, `Deactivation`, `Communication` и др. Управление только через предложения.
|
||||||
- Детерминированные адреса: фабрика `FactoryDeployer` + CREATE2. Единый адрес DLE и модулей во всех выбранных сетях. INIT_CODE_HASH автоподставляется из актуального initCode.
|
- Детерминированные адреса: CREATE с выровненным nonce. Единый адрес DLE и модулей во всех выбранных сетях.
|
||||||
- Аналитика: добавлены view‑функции для агрегирования и пагинации.
|
- Аналитика: добавлены view‑функции для агрегирования и пагинации.
|
||||||
|
|
||||||
Пример основных функций DLE v2 (интерфейс):
|
Пример основных функций DLE v2 (интерфейс):
|
||||||
|
|||||||
@@ -12,10 +12,11 @@
|
|||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// Создаем экземпляр axios с базовым URL
|
// Создаем экземпляр axios с базовым URL и таймаутами
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
timeout: 10 * 60 * 1000, // 10 минут таймаут для деплоя
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@@ -25,15 +26,36 @@ const api = axios.create({
|
|||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
config.withCredentials = true; // Важно для каждого запроса
|
config.withCredentials = true; // Важно для каждого запроса
|
||||||
|
|
||||||
|
// DEBUG: логируем все исходящие запросы
|
||||||
|
console.log('🌐 [AXIOS] Отправляем запрос:', {
|
||||||
|
method: config.method?.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
fullURL: config.baseURL + config.url,
|
||||||
|
data: config.data ? '[ДАННЫЕ]' : 'нет данных'
|
||||||
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => {
|
||||||
|
console.error('🌐 [AXIOS] Ошибка перед отправкой:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Добавляем перехватчик ответов для обработки ошибок
|
// Добавляем перехватчик ответов для обработки ошибок
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
|
// DEBUG: логируем успешные ответы
|
||||||
|
console.log('🌐 [AXIOS] Получен ответ:', {
|
||||||
|
method: response.config.method?.toUpperCase(),
|
||||||
|
url: response.config.url,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
contentType: response.headers['content-type']
|
||||||
|
});
|
||||||
|
|
||||||
// Проверяем, что ответ действительно JSON
|
// Проверяем, что ответ действительно JSON
|
||||||
if (response.headers['content-type'] &&
|
if (response.headers['content-type'] &&
|
||||||
!response.headers['content-type'].includes('application/json')) {
|
!response.headers['content-type'].includes('application/json')) {
|
||||||
@@ -46,6 +68,16 @@ api.interceptors.response.use(
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
// DEBUG: логируем ошибки
|
||||||
|
console.error('🌐 [AXIOS] Ошибка ответа:', {
|
||||||
|
method: error.config?.method?.toUpperCase(),
|
||||||
|
url: error.config?.url,
|
||||||
|
message: error.message,
|
||||||
|
code: error.code,
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText
|
||||||
|
});
|
||||||
|
|
||||||
// Если ошибка содержит HTML в response
|
// Если ошибка содержит HTML в response
|
||||||
if (error.response && error.response.data &&
|
if (error.response && error.response.data &&
|
||||||
typeof error.response.data === 'string' &&
|
typeof error.response.data === 'string' &&
|
||||||
|
|||||||
601
frontend/src/components/deployment/DeploymentWizard.vue
Normal file
601
frontend/src/components/deployment/DeploymentWizard.vue
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is proprietary and confidential.
|
||||||
|
Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
|
||||||
|
For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
Website: https://hb3-accelerator.com
|
||||||
|
GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="deployment-wizard">
|
||||||
|
<!-- Заголовок -->
|
||||||
|
<div class="wizard-header">
|
||||||
|
<h2 class="wizard-title">Мастер поэтапного деплоя DLE</h2>
|
||||||
|
<p class="wizard-subtitle">
|
||||||
|
Автоматический деплой DLE контракта и модулей с WebSocket обновлениями в реальном времени
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Прогресс-бар -->
|
||||||
|
<div class="progress-section">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: `${progressPercentage}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-text">
|
||||||
|
{{ currentStage }} ({{ progressPercentage }}%)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статус деплоя -->
|
||||||
|
<div class="status-section">
|
||||||
|
<div class="status-card" :class="statusClass">
|
||||||
|
<div class="status-icon">
|
||||||
|
<i :class="statusIcon"></i>
|
||||||
|
</div>
|
||||||
|
<div class="status-content">
|
||||||
|
<h3 class="status-title">{{ statusTitle }}</h3>
|
||||||
|
<p class="status-message">{{ statusMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Логи операций -->
|
||||||
|
<div class="logs-section">
|
||||||
|
<div class="logs-header">
|
||||||
|
<h3>Логи операций</h3>
|
||||||
|
<button
|
||||||
|
class="clear-logs-btn"
|
||||||
|
@click="clearLogs"
|
||||||
|
:disabled="isDeploying"
|
||||||
|
>
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="logs-container" ref="logsContainer">
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in logs"
|
||||||
|
:key="index"
|
||||||
|
:class="['log-entry', `log-${log.type}`]"
|
||||||
|
>
|
||||||
|
<span class="log-time">{{ log.timestamp }}</span>
|
||||||
|
<span class="log-message">{{ log.message }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="logs.length === 0" class="no-logs">
|
||||||
|
Логи операций будут отображаться здесь
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сетевые статусы -->
|
||||||
|
<div v-if="Object.keys(networksStatus).length > 0" class="networks-section">
|
||||||
|
<h3>Статус по сетям</h3>
|
||||||
|
<div class="networks-grid">
|
||||||
|
<div
|
||||||
|
v-for="(network, chainId) in networksStatus"
|
||||||
|
:key="chainId"
|
||||||
|
:class="['network-item', `network-${network.status}`]"
|
||||||
|
>
|
||||||
|
<div class="network-name">{{ getNetworkName(chainId) }}</div>
|
||||||
|
<div class="network-status">{{ network.status }}</div>
|
||||||
|
<div v-if="network.address" class="network-address">{{ network.address.substring(0, 10) }}...</div>
|
||||||
|
<div v-if="network.message" class="network-message">{{ network.message }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопки управления -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<button
|
||||||
|
class="stop-btn"
|
||||||
|
@click="stopDeploymentTracking"
|
||||||
|
v-if="isDeploying"
|
||||||
|
>
|
||||||
|
<i class="fas fa-stop"></i>
|
||||||
|
Остановить отслеживание
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="reset-btn"
|
||||||
|
@click="resetDeploymentState"
|
||||||
|
v-if="deploymentStatus === 'completed' || deploymentStatus === 'failed'"
|
||||||
|
>
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
Сбросить состояние
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ошибка -->
|
||||||
|
<div v-if="error" class="error-section">
|
||||||
|
<div class="error-card">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<div>
|
||||||
|
<h4>Произошла ошибка</h4>
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, nextTick, onMounted } from 'vue';
|
||||||
|
import { useDeploymentWebSocket } from '@/composables/useDeploymentWebSocket';
|
||||||
|
import api from '@/api/axios';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
dleAddress: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
privateKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedNetworks: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
dleData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
etherscanApiKey: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events
|
||||||
|
const emit = defineEmits(['deployment-completed']);
|
||||||
|
|
||||||
|
// WebSocket композабл для деплоя
|
||||||
|
const {
|
||||||
|
deploymentStatus,
|
||||||
|
currentStage,
|
||||||
|
progress,
|
||||||
|
isDeploying,
|
||||||
|
logs,
|
||||||
|
deploymentResult,
|
||||||
|
networksStatus,
|
||||||
|
error,
|
||||||
|
startDeploymentTracking,
|
||||||
|
stopDeploymentTracking,
|
||||||
|
resetDeploymentState,
|
||||||
|
addLog,
|
||||||
|
clearLogs
|
||||||
|
} = useDeploymentWebSocket();
|
||||||
|
|
||||||
|
// Ссылка на контейнер логов
|
||||||
|
const logsContainer = ref(null);
|
||||||
|
|
||||||
|
// Вычисляемые свойства
|
||||||
|
const progressPercentage = computed(() => {
|
||||||
|
return Math.round((progress.value || 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
switch (deploymentStatus.value) {
|
||||||
|
case 'completed': return 'status-success';
|
||||||
|
case 'failed': return 'status-error';
|
||||||
|
case 'in_progress': return 'status-running';
|
||||||
|
default: return 'status-pending';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusIcon = computed(() => {
|
||||||
|
switch (deploymentStatus.value) {
|
||||||
|
case 'completed': return 'fas fa-check-circle';
|
||||||
|
case 'failed': return 'fas fa-times-circle';
|
||||||
|
case 'in_progress': return 'fas fa-spinner fa-spin';
|
||||||
|
default: return 'fas fa-clock';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusTitle = computed(() => {
|
||||||
|
switch (deploymentStatus.value) {
|
||||||
|
case 'not_started': return 'Готов к запуску';
|
||||||
|
case 'in_progress': return 'Выполняется деплой';
|
||||||
|
case 'completed': return 'Деплой завершен';
|
||||||
|
case 'failed': return 'Ошибка деплоя';
|
||||||
|
default: return 'Неизвестный статус';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusMessage = computed(() => {
|
||||||
|
switch (deploymentStatus.value) {
|
||||||
|
case 'not_started': return 'Готов к автоматическому развертыванию через WebSocket';
|
||||||
|
case 'in_progress': return `Выполняется: ${currentStage.value || 'инициализация'}`;
|
||||||
|
case 'completed': return 'Все этапы деплоя успешно завершены!';
|
||||||
|
case 'failed': return 'Произошла ошибка. Проверьте логи для деталей.';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функции
|
||||||
|
const getNetworkName = (chainId) => {
|
||||||
|
const networkNames = {
|
||||||
|
'1': 'Ethereum',
|
||||||
|
'11155111': 'Sepolia',
|
||||||
|
'421614': 'Arbitrum Sepolia',
|
||||||
|
'84532': 'Base Sepolia',
|
||||||
|
'17000': 'Holesky'
|
||||||
|
};
|
||||||
|
return networkNames[chainId] || `Network ${chainId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (logsContainer.value) {
|
||||||
|
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Главная функция запуска деплоя
|
||||||
|
const startDeployment = async () => {
|
||||||
|
try {
|
||||||
|
addLog('🚀 Начинаем асинхронный деплой с WebSocket отслеживанием', 'info');
|
||||||
|
|
||||||
|
// Подготовка данных для деплоя
|
||||||
|
const deployData = {
|
||||||
|
name: props.dleData.name,
|
||||||
|
symbol: props.dleData.tokenSymbol,
|
||||||
|
location: props.dleData.addressData?.fullAddress || 'Не указан',
|
||||||
|
coordinates: props.dleData.coordinates || '0,0',
|
||||||
|
jurisdiction: parseInt(props.dleData.jurisdiction) || 0,
|
||||||
|
oktmo: props.dleData.selectedOktmo || '',
|
||||||
|
okvedCodes: props.dleData.selectedOkved || [],
|
||||||
|
kpp: props.dleData.kppCode || '',
|
||||||
|
quorumPercentage: props.dleData.governanceQuorum || 51,
|
||||||
|
initialPartners: props.dleData.partners.map(p => p.address).filter(addr => addr),
|
||||||
|
initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0),
|
||||||
|
supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined),
|
||||||
|
currentChainId: props.selectedNetworks[0] || 1,
|
||||||
|
privateKey: props.privateKey,
|
||||||
|
etherscanApiKey: props.etherscanApiKey || '',
|
||||||
|
autoVerifyAfterDeploy: false
|
||||||
|
};
|
||||||
|
|
||||||
|
addLog('📤 Отправляем запрос на асинхронный деплой...', 'info');
|
||||||
|
|
||||||
|
// Отправляем запрос на асинхронный деплой (без таймаута!)
|
||||||
|
const response = await api.post('/dle-v2', deployData);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.deploymentId) {
|
||||||
|
addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success');
|
||||||
|
|
||||||
|
// Начинаем отслеживание через WebSocket
|
||||||
|
startDeploymentTracking(response.data.deploymentId);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка'));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`❌ Ошибка запуска деплоя: ${error.message}`, 'error');
|
||||||
|
console.error('Deployment start failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Автозапуск деплоя при появлении компонента
|
||||||
|
onMounted(() => {
|
||||||
|
if (deploymentStatus.value === 'not_started') {
|
||||||
|
addLog('🚀 Автоматически запускаем деплой...', 'info');
|
||||||
|
startDeployment();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Следим за новыми логами и скроллим вниз
|
||||||
|
watch(logs, () => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Следим за завершением деплоя
|
||||||
|
watch(deploymentStatus, (newStatus) => {
|
||||||
|
if (newStatus === 'completed' && deploymentResult.value) {
|
||||||
|
addLog('🎉 Деплой успешно завершен! Перенаправляем на страницу управления...', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
emit('deployment-completed', deploymentResult.value);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.deployment-wizard {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-title {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-subtitle {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #ecf0f1;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #3498db, #2ecc71);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 2px solid #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.status-pending {
|
||||||
|
border-color: #f39c12;
|
||||||
|
background-color: #fef9e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.status-running {
|
||||||
|
border-color: #3498db;
|
||||||
|
background-color: #ebf3fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.status-success {
|
||||||
|
border-color: #2ecc71;
|
||||||
|
background-color: #eafaf1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card.status-error {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
background-color: #fdf2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content p {
|
||||||
|
margin: 0;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-logs-btn {
|
||||||
|
background: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-logs-btn:hover:not(:disabled) {
|
||||||
|
background: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-container {
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #bdc3c7;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #95a5a6;
|
||||||
|
font-size: 0.9em;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-info { color: #3498db; }
|
||||||
|
.log-success { color: #2ecc71; }
|
||||||
|
.log-error { color: #e74c3c; }
|
||||||
|
.log-warning { color: #f39c12; }
|
||||||
|
|
||||||
|
.no-logs {
|
||||||
|
text-align: center;
|
||||||
|
color: #95a5a6;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #bdc3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item.network-pending {
|
||||||
|
border-color: #f39c12;
|
||||||
|
background-color: #fef9e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item.network-in_progress {
|
||||||
|
border-color: #3498db;
|
||||||
|
background-color: #ebf3fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item.network-completed {
|
||||||
|
border-color: #2ecc71;
|
||||||
|
background-color: #eafaf1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item.network-failed {
|
||||||
|
border-color: #e74c3c;
|
||||||
|
background-color: #fdf2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-address {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-message {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn, .stop-btn, .reset-btn {
|
||||||
|
padding: 15px 30px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, #27ae60, #219a52);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn:disabled {
|
||||||
|
background: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn {
|
||||||
|
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #c0392b, #a93226);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #2980b9, #21618c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fdf2f2;
|
||||||
|
border: 2px solid #e74c3c;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card i {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -60,7 +60,6 @@ export default function useBlockchainNetworks() {
|
|||||||
{ value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 },
|
{ value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 },
|
||||||
{ value: 'arbitrum-sepolia', label: 'Arbitrum Sepolia', chainId: 421614 },
|
{ value: 'arbitrum-sepolia', label: 'Arbitrum Sepolia', chainId: 421614 },
|
||||||
{ value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 },
|
{ value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 },
|
||||||
{ value: 'avalanche-fuji', label: 'Avalanche Fuji', chainId: 43113 },
|
|
||||||
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 },
|
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 },
|
||||||
{ value: 'base-sepolia', label: 'Base Sepolia Testnet', chainId: 84532 }
|
{ value: 'base-sepolia', label: 'Base Sepolia Testnet', chainId: 84532 }
|
||||||
]
|
]
|
||||||
|
|||||||
210
frontend/src/composables/useDeploymentWebSocket.js
Normal file
210
frontend/src/composables/useDeploymentWebSocket.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ref, reactive, onUnmounted } from 'vue';
|
||||||
|
import wsClient from '../utils/websocket';
|
||||||
|
|
||||||
|
export function useDeploymentWebSocket() {
|
||||||
|
// Состояние деплоя
|
||||||
|
const deploymentStatus = ref('not_started'); // not_started, in_progress, completed, failed
|
||||||
|
const currentStage = ref('');
|
||||||
|
const currentNetwork = ref('');
|
||||||
|
const progress = ref(0);
|
||||||
|
const isDeploying = ref(false);
|
||||||
|
const deploymentId = ref(null);
|
||||||
|
const logs = ref([]);
|
||||||
|
const error = ref(null);
|
||||||
|
|
||||||
|
// Детальная информация по сетям
|
||||||
|
const networksStatus = reactive({});
|
||||||
|
|
||||||
|
// Результат деплоя
|
||||||
|
const deploymentResult = ref(null);
|
||||||
|
|
||||||
|
// Добавить лог
|
||||||
|
const addLog = (message, type = 'info') => {
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
logs.value.push({
|
||||||
|
timestamp,
|
||||||
|
message,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Очистить логи
|
||||||
|
const clearLogs = () => {
|
||||||
|
logs.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик WebSocket сообщений
|
||||||
|
const handleDeploymentUpdate = (data) => {
|
||||||
|
if (data.deploymentId !== deploymentId.value) return;
|
||||||
|
|
||||||
|
console.log('🔄 [DeploymentWebSocket] Получено обновление:', data);
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'deployment_started':
|
||||||
|
deploymentStatus.value = 'in_progress';
|
||||||
|
isDeploying.value = true;
|
||||||
|
currentStage.value = data.stage || '';
|
||||||
|
addLog(`🚀 ${data.message}`, 'info');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_progress':
|
||||||
|
currentStage.value = data.stage || '';
|
||||||
|
currentNetwork.value = data.network || '';
|
||||||
|
progress.value = data.progress || 0;
|
||||||
|
if (data.message) {
|
||||||
|
addLog(`📊 ${data.message}`, 'info');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_stage_completed':
|
||||||
|
if (data.message) {
|
||||||
|
addLog(`✅ ${data.message}`, 'success');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_network_update':
|
||||||
|
if (data.network) {
|
||||||
|
networksStatus[data.network] = {
|
||||||
|
status: data.status,
|
||||||
|
address: data.address,
|
||||||
|
message: data.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data.message) {
|
||||||
|
addLog(`🌐 [${data.network}] ${data.message}`, 'info');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_error':
|
||||||
|
error.value = data.error;
|
||||||
|
if (data.message) {
|
||||||
|
addLog(`❌ ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_completed':
|
||||||
|
deploymentStatus.value = 'completed';
|
||||||
|
isDeploying.value = false;
|
||||||
|
deploymentResult.value = data.result;
|
||||||
|
progress.value = 100;
|
||||||
|
addLog(`🎉 ${data.message}`, 'success');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_failed':
|
||||||
|
deploymentStatus.value = 'failed';
|
||||||
|
isDeploying.value = false;
|
||||||
|
error.value = data.error;
|
||||||
|
addLog(`💥 ${data.message}`, 'error');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'deployment_log':
|
||||||
|
if (data.log) {
|
||||||
|
addLog(data.log.message, data.log.type || 'info');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case undefined:
|
||||||
|
// Обработка событий без типа (прямые обновления)
|
||||||
|
if (data.stage) currentStage.value = data.stage;
|
||||||
|
if (data.progress !== undefined) progress.value = data.progress;
|
||||||
|
if (data.status) deploymentStatus.value = data.status;
|
||||||
|
if (data.result) deploymentResult.value = data.result;
|
||||||
|
if (data.error) error.value = data.error;
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
isDeploying.value = false;
|
||||||
|
addLog('🎉 Деплой успешно завершен!', 'success');
|
||||||
|
} else if (data.status === 'failed') {
|
||||||
|
isDeploying.value = false;
|
||||||
|
addLog('💥 Деплой завершился с ошибкой!', 'error');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn('🤷♂️ [DeploymentWebSocket] Неизвестный тип события:', data.type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Начать отслеживание деплоя
|
||||||
|
const startDeploymentTracking = (id) => {
|
||||||
|
console.log('🎯 [DeploymentWebSocket] Начинаем отслеживание деплоя:', id);
|
||||||
|
|
||||||
|
deploymentId.value = id;
|
||||||
|
deploymentStatus.value = 'in_progress';
|
||||||
|
isDeploying.value = true;
|
||||||
|
clearLogs();
|
||||||
|
|
||||||
|
// Подключаемся к WebSocket обновлениям
|
||||||
|
wsClient.connect();
|
||||||
|
if (wsClient && typeof wsClient.subscribe === 'function') {
|
||||||
|
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
|
||||||
|
} else {
|
||||||
|
console.warn('[DeploymentWebSocket] wsClient.subscribe недоступен');
|
||||||
|
}
|
||||||
|
|
||||||
|
addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Остановить отслеживание
|
||||||
|
const stopDeploymentTracking = () => {
|
||||||
|
console.log('🛑 [DeploymentWebSocket] Останавливаем отслеживание');
|
||||||
|
|
||||||
|
if (wsClient && typeof wsClient.unsubscribe === 'function') {
|
||||||
|
wsClient.unsubscribe('deployment_update', handleDeploymentUpdate);
|
||||||
|
} else {
|
||||||
|
console.warn('[DeploymentWebSocket] wsClient.unsubscribe недоступен');
|
||||||
|
}
|
||||||
|
isDeploying.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Очистить состояние
|
||||||
|
const resetDeploymentState = () => {
|
||||||
|
deploymentStatus.value = 'not_started';
|
||||||
|
currentStage.value = '';
|
||||||
|
currentNetwork.value = '';
|
||||||
|
progress.value = 0;
|
||||||
|
isDeploying.value = false;
|
||||||
|
deploymentId.value = null;
|
||||||
|
error.value = null;
|
||||||
|
deploymentResult.value = null;
|
||||||
|
clearLogs();
|
||||||
|
Object.keys(networksStatus).forEach(key => delete networksStatus[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Автоматическая отписка при размонтировании компонента
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopDeploymentTracking();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Состояние
|
||||||
|
deploymentStatus,
|
||||||
|
currentStage,
|
||||||
|
currentNetwork,
|
||||||
|
progress,
|
||||||
|
isDeploying,
|
||||||
|
deploymentId,
|
||||||
|
logs,
|
||||||
|
error,
|
||||||
|
networksStatus,
|
||||||
|
deploymentResult,
|
||||||
|
|
||||||
|
// Методы
|
||||||
|
startDeploymentTracking,
|
||||||
|
stopDeploymentTracking,
|
||||||
|
resetDeploymentState,
|
||||||
|
addLog,
|
||||||
|
clearLogs
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -242,6 +242,11 @@ const routes = [
|
|||||||
name: 'module-deploy-timelock',
|
name: 'module-deploy-timelock',
|
||||||
component: () => import('../views/smartcontracts/modules/TimelockModuleDeployView.vue')
|
component: () => import('../views/smartcontracts/modules/TimelockModuleDeployView.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/management/modules/deploy/reader',
|
||||||
|
name: 'module-deploy-reader',
|
||||||
|
component: () => import('../views/smartcontracts/modules/DLEReaderDeployView.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/management/modules/deploy/communication',
|
path: '/management/modules/deploy/communication',
|
||||||
name: 'module-deploy-communication',
|
name: 'module-deploy-communication',
|
||||||
|
|||||||
@@ -15,20 +15,6 @@ import axios from 'axios';
|
|||||||
|
|
||||||
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
|
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
|
||||||
|
|
||||||
/**
|
|
||||||
* Создает новое DLE v2
|
|
||||||
* @param {Object} dleParams - Параметры DLE
|
|
||||||
* @returns {Promise<Object>} - Результат создания
|
|
||||||
*/
|
|
||||||
export const createDLE = async (dleParams) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/dle-v2', dleParams);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при создании DLE:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает список всех DLE v2
|
* Получает список всех DLE v2
|
||||||
@@ -59,34 +45,7 @@ export const getDLEInfo = async (dleAddress) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Получает параметры по умолчанию для создания DLE v2
|
|
||||||
* @returns {Promise<Object>} - Параметры по умолчанию
|
|
||||||
*/
|
|
||||||
export const getDefaultParams = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get('/dle-v2/default-params');
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при получении параметров по умолчанию:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Читает данные DLE из блокчейна
|
|
||||||
* @param {string} dleAddress - Адрес DLE
|
|
||||||
* @returns {Promise<Object>} - Данные из блокчейна
|
|
||||||
*/
|
|
||||||
export const readDLEFromBlockchain = async (dleAddress) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/dle-core/read-dle-info', { dleAddress });
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при чтении DLE из блокчейна:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает параметры управления DLE
|
* Получает параметры управления DLE
|
||||||
@@ -128,35 +87,12 @@ export const getSupportedChains = async (dleAddress) => {
|
|||||||
* @param {number} chainId - ID сети
|
* @param {number} chainId - ID сети
|
||||||
* @returns {Promise<Object>} - Статус поддержки
|
* @returns {Promise<Object>} - Статус поддержки
|
||||||
*/
|
*/
|
||||||
export const isChainSupported = async (dleAddress, chainId) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/dle-multichain/is-chain-supported', {
|
|
||||||
dleAddress,
|
|
||||||
chainId
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке поддержки сети:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает текущую сеть
|
* Получает текущую сеть
|
||||||
* @param {string} dleAddress - Адрес DLE
|
* @param {string} dleAddress - Адрес DLE
|
||||||
* @returns {Promise<Object>} - Текущая сеть
|
* @returns {Promise<Object>} - Текущая сеть
|
||||||
*/
|
*/
|
||||||
export const getCurrentChainId = async (dleAddress) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/blockchain/get-current-chain-id', {
|
|
||||||
dleAddress
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при получении текущей сети:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Исполняет предложение по подписям
|
* Исполняет предложение по подписям
|
||||||
@@ -164,18 +100,6 @@ export const getCurrentChainId = async (dleAddress) => {
|
|||||||
* @param {Object} executionData - Данные исполнения
|
* @param {Object} executionData - Данные исполнения
|
||||||
* @returns {Promise<Object>} - Результат исполнения
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
*/
|
*/
|
||||||
export const executeProposalBySignatures = async (dleAddress, executionData) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/dle-multichain/execute-proposal-by-signatures', {
|
|
||||||
dleAddress,
|
|
||||||
...executionData
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при исполнении предложения по подписям:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== ИСТОРИЯ И СОБЫТИЯ =====
|
// ===== ИСТОРИЯ И СОБЫТИЯ =====
|
||||||
|
|
||||||
@@ -187,34 +111,9 @@ export const executeProposalBySignatures = async (dleAddress, executionData) =>
|
|||||||
* @param {number} toBlock - Конечный блок
|
* @param {number} toBlock - Конечный блок
|
||||||
* @returns {Promise<Object>} - История событий
|
* @returns {Promise<Object>} - История событий
|
||||||
*/
|
*/
|
||||||
export const getEventHistory = async (dleAddress, eventType, fromBlock, toBlock) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/blockchain/get-event-history', {
|
|
||||||
dleAddress,
|
|
||||||
eventType,
|
|
||||||
fromBlock,
|
|
||||||
toBlock
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при получении истории событий:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает статистику DLE
|
* Получает статистику DLE
|
||||||
* @param {string} dleAddress - Адрес DLE
|
* @param {string} dleAddress - Адрес DLE
|
||||||
* @returns {Promise<Object>} - Статистика
|
* @returns {Promise<Object>} - Статистика
|
||||||
*/
|
*/
|
||||||
export const getDLEStats = async (dleAddress) => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/blockchain/get-dle-stats', {
|
|
||||||
dleAddress
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при получении статистики DLE:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Сервис для работы с модулями DLE
|
// Сервис для работы с модулями DLE
|
||||||
import axios from 'axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Создает предложение о добавлении модуля
|
* Создает предложение о добавлении модуля
|
||||||
@@ -21,7 +21,7 @@ import axios from 'axios';
|
|||||||
*/
|
*/
|
||||||
export const createAddModuleProposal = async (dleAddress, moduleData) => {
|
export const createAddModuleProposal = async (dleAddress, moduleData) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-modules/create-add-module-proposal', {
|
const response = await api.post('/dle-modules/create-add-module-proposal', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
...moduleData
|
...moduleData
|
||||||
});
|
});
|
||||||
@@ -40,7 +40,7 @@ export const createAddModuleProposal = async (dleAddress, moduleData) => {
|
|||||||
*/
|
*/
|
||||||
export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
|
export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-modules/create-remove-module-proposal', {
|
const response = await api.post('/dle-modules/create-remove-module-proposal', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
...moduleData
|
...moduleData
|
||||||
});
|
});
|
||||||
@@ -59,7 +59,7 @@ export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
|
|||||||
*/
|
*/
|
||||||
export const isModuleActive = async (dleAddress, moduleId) => {
|
export const isModuleActive = async (dleAddress, moduleId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-modules/is-module-active', {
|
const response = await api.post('/dle-modules/is-module-active', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId
|
moduleId
|
||||||
});
|
});
|
||||||
@@ -76,11 +76,12 @@ export const isModuleActive = async (dleAddress, moduleId) => {
|
|||||||
* @param {string} moduleId - ID модуля
|
* @param {string} moduleId - ID модуля
|
||||||
* @returns {Promise<Object>} - Адрес модуля
|
* @returns {Promise<Object>} - Адрес модуля
|
||||||
*/
|
*/
|
||||||
export const getModuleAddress = async (dleAddress, moduleId) => {
|
export const getModuleAddress = async (dleAddress, moduleId, chainId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-modules/get-module-address', {
|
const response = await api.post('/dle-modules/get-module-address', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId
|
moduleId,
|
||||||
|
chainId
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -96,7 +97,7 @@ export const getModuleAddress = async (dleAddress, moduleId) => {
|
|||||||
*/
|
*/
|
||||||
export const getAllModules = async (dleAddress) => {
|
export const getAllModules = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-modules/get-all-modules', {
|
const response = await api.post('/dle-modules/get-all-modules', {
|
||||||
dleAddress
|
dleAddress
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -106,6 +107,23 @@ export const getAllModules = async (dleAddress) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает информацию о поддерживаемых сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @returns {Promise<Object>} - Информация о сетях
|
||||||
|
*/
|
||||||
|
export const getNetworksInfo = async (dleAddress) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/get-networks-info', {
|
||||||
|
dleAddress
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении информации о сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает информацию о модуле
|
* Получает информацию о модуле
|
||||||
* @param {string} dleAddress - Адрес DLE
|
* @param {string} dleAddress - Адрес DLE
|
||||||
@@ -114,7 +132,7 @@ export const getAllModules = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const getModuleInfo = async (dleAddress, moduleId) => {
|
export const getModuleInfo = async (dleAddress, moduleId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-module-info', {
|
const response = await api.post('/blockchain/get-module-info', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId
|
moduleId
|
||||||
});
|
});
|
||||||
@@ -132,7 +150,7 @@ export const getModuleInfo = async (dleAddress, moduleId) => {
|
|||||||
*/
|
*/
|
||||||
export const getModulesStats = async (dleAddress) => {
|
export const getModulesStats = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-modules-stats', {
|
const response = await api.post('/blockchain/get-modules-stats', {
|
||||||
dleAddress
|
dleAddress
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -150,7 +168,7 @@ export const getModulesStats = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const getModulesHistory = async (dleAddress, filters = {}) => {
|
export const getModulesHistory = async (dleAddress, filters = {}) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-modules-history', {
|
const response = await api.post('/blockchain/get-modules-history', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
...filters
|
...filters
|
||||||
});
|
});
|
||||||
@@ -168,7 +186,7 @@ export const getModulesHistory = async (dleAddress, filters = {}) => {
|
|||||||
*/
|
*/
|
||||||
export const getActiveModules = async (dleAddress) => {
|
export const getActiveModules = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-active-modules', {
|
const response = await api.post('/blockchain/get-active-modules', {
|
||||||
dleAddress
|
dleAddress
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -185,7 +203,7 @@ export const getActiveModules = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const getInactiveModules = async (dleAddress) => {
|
export const getInactiveModules = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-inactive-modules', {
|
const response = await api.post('/blockchain/get-inactive-modules', {
|
||||||
dleAddress
|
dleAddress
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -204,7 +222,7 @@ export const getInactiveModules = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddress) => {
|
export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/check-module-compatibility', {
|
const response = await api.post('/blockchain/check-module-compatibility', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId,
|
moduleId,
|
||||||
moduleAddress
|
moduleAddress
|
||||||
@@ -224,7 +242,7 @@ export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddre
|
|||||||
*/
|
*/
|
||||||
export const getModuleConfig = async (dleAddress, moduleId) => {
|
export const getModuleConfig = async (dleAddress, moduleId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-module-config', {
|
const response = await api.post('/blockchain/get-module-config', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId
|
moduleId
|
||||||
});
|
});
|
||||||
@@ -244,7 +262,7 @@ export const getModuleConfig = async (dleAddress, moduleId) => {
|
|||||||
*/
|
*/
|
||||||
export const updateModuleConfig = async (dleAddress, moduleId, config) => {
|
export const updateModuleConfig = async (dleAddress, moduleId, config) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/update-module-config', {
|
const response = await api.post('/blockchain/update-module-config', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId,
|
moduleId,
|
||||||
config
|
config
|
||||||
@@ -265,7 +283,7 @@ export const updateModuleConfig = async (dleAddress, moduleId, config) => {
|
|||||||
*/
|
*/
|
||||||
export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
|
export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-module-events', {
|
const response = await api.post('/blockchain/get-module-events', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId,
|
moduleId,
|
||||||
...filters
|
...filters
|
||||||
@@ -285,7 +303,7 @@ export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
|
|||||||
*/
|
*/
|
||||||
export const getModulePerformance = async (dleAddress, moduleId) => {
|
export const getModulePerformance = async (dleAddress, moduleId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-module-performance', {
|
const response = await api.post('/blockchain/get-module-performance', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
moduleId
|
moduleId
|
||||||
});
|
});
|
||||||
@@ -295,3 +313,231 @@ export const getModulePerformance = async (dleAddress, moduleId) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализирует модули во всех сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @returns {Promise<Object>} - Результат инициализации
|
||||||
|
*/
|
||||||
|
export const initializeModulesAllNetworks = async (dleAddress, privateKey) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/initialize-modules-all-networks', {
|
||||||
|
dleAddress,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при инициализации модулей во всех сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Верифицирует модули во всех сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @returns {Promise<Object>} - Результат верификации
|
||||||
|
*/
|
||||||
|
export const verifyModulesAllNetworks = async (dleAddress, privateKey) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/verify-modules-all-networks', {
|
||||||
|
dleAddress,
|
||||||
|
privateKey
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при верификации модулей во всех сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет статус деплоя DLE контракта
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {Array<number>} chainIds - Список ID сетей
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Статус деплоя DLE
|
||||||
|
*/
|
||||||
|
export const checkDLEDeploymentStatus = async (dleAddress, chainIds, maxRetries = 3, retryDelay = 30000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/check-dle-deployment-status', {
|
||||||
|
dleAddress,
|
||||||
|
chainIds,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке статуса деплоя DLE:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет статус деплоя модуля
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||||
|
* @param {Array<number>} chainIds - Список ID сетей
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Статус деплоя модуля
|
||||||
|
*/
|
||||||
|
export const checkModuleDeploymentStatus = async (dleAddress, moduleType, chainIds, maxRetries = 3, retryDelay = 30000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/check-module-deployment-status', {
|
||||||
|
dleAddress,
|
||||||
|
moduleType,
|
||||||
|
chainIds,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при проверке статуса деплоя модуля:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Деплоит модуль во всех сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Результат деплоя модуля
|
||||||
|
*/
|
||||||
|
export const deployModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 45000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/deploy-module-all-networks', {
|
||||||
|
dleAddress,
|
||||||
|
moduleType,
|
||||||
|
privateKey,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при деплое модуля во всех сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Верифицирует DLE контракт во всех сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Результат верификации DLE
|
||||||
|
*/
|
||||||
|
export const verifyDLEAllNetworks = async (dleAddress, privateKey, maxRetries = 3, retryDelay = 60000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/verify-dle-all-networks', {
|
||||||
|
dleAddress,
|
||||||
|
privateKey,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при верификации DLE во всех сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Верифицирует модуль во всех сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Результат верификации модуля
|
||||||
|
*/
|
||||||
|
export const verifyModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 60000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/verify-module-all-networks', {
|
||||||
|
dleAddress,
|
||||||
|
moduleType,
|
||||||
|
privateKey,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при верификации модуля во всех сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализирует модуль во всех сетях
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||||
|
* @param {string} privateKey - Приватный ключ
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Результат инициализации модуля
|
||||||
|
*/
|
||||||
|
export const initializeModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 30000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/initialize-module-all-networks', {
|
||||||
|
dleAddress,
|
||||||
|
moduleType,
|
||||||
|
privateKey,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при инициализации модуля во всех сетях:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет финальную проверку готовности деплоя
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {Array<number>} chainIds - Список ID сетей
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Результат финальной проверки
|
||||||
|
*/
|
||||||
|
export const finalDeploymentCheck = async (dleAddress, chainIds, maxRetries = 3, retryDelay = 30000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/final-deployment-check', {
|
||||||
|
dleAddress,
|
||||||
|
chainIds,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при финальной проверке деплоя:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает общий статус деплоя
|
||||||
|
* @param {string} dleAddress - Адрес DLE
|
||||||
|
* @param {number} maxRetries - Максимальное количество попыток
|
||||||
|
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||||
|
* @returns {Promise<Object>} - Статус деплоя
|
||||||
|
*/
|
||||||
|
export const getDeploymentStatus = async (dleAddress, maxRetries = 3, retryDelay = 30000) => {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/dle-modules/get-deployment-status', {
|
||||||
|
dleAddress,
|
||||||
|
maxRetries,
|
||||||
|
retryDelay
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении статуса деплоя:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -261,3 +261,20 @@ export const getQuorumAt = async (dleAddress, timepoint) => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Декодирует данные предложения о добавлении модуля
|
||||||
|
* @param {string} transactionHash - Хеш транзакции создания предложения
|
||||||
|
* @returns {Promise<Object>} - Декодированные данные предложения
|
||||||
|
*/
|
||||||
|
export const decodeProposalData = async (transactionHash) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/dle-proposals/decode-proposal-data', {
|
||||||
|
transactionHash
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при декодировании данных предложения:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class WebSocketService {
|
|||||||
try {
|
try {
|
||||||
// Определяем WebSocket URL
|
// Определяем WebSocket URL
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
// В Docker окружении используем тот же хост, что и для HTTP
|
// Подключаемся к бэкенду через Vite proxy
|
||||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
// console.log('🔌 [WebSocket] Подключение к:', wsUrl);
|
// console.log('🔌 [WebSocket] Подключение к:', wsUrl);
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import axios from 'axios';
|
/**
|
||||||
|
* 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';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +67,7 @@ export async function checkWalletConnection() {
|
|||||||
*/
|
*/
|
||||||
export async function getDLEInfo(dleAddress) {
|
export async function getDLEInfo(dleAddress) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:8000/api/blockchain/read-dle-info', {
|
const response = await api.post('/blockchain/read-dle-info', {
|
||||||
dleAddress: dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +244,7 @@ export async function executeProposal(dleAddress, proposalId) {
|
|||||||
*/
|
*/
|
||||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/create-add-module-proposal', {
|
const response = await api.post('/blockchain/create-add-module-proposal', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
description: description,
|
description: description,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
@@ -263,7 +275,7 @@ export async function createAddModuleProposal(dleAddress, description, duration,
|
|||||||
*/
|
*/
|
||||||
export async function createRemoveModuleProposal(dleAddress, description, duration, moduleId, chainId) {
|
export async function createRemoveModuleProposal(dleAddress, description, duration, moduleId, chainId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/create-remove-module-proposal', {
|
const response = await api.post('/blockchain/create-remove-module-proposal', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
description: description,
|
description: description,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
@@ -290,7 +302,7 @@ export async function createRemoveModuleProposal(dleAddress, description, durati
|
|||||||
*/
|
*/
|
||||||
export async function isModuleActive(dleAddress, moduleId) {
|
export async function isModuleActive(dleAddress, moduleId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/is-module-active', {
|
const response = await api.post('/blockchain/is-module-active', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
moduleId: moduleId
|
moduleId: moduleId
|
||||||
});
|
});
|
||||||
@@ -312,11 +324,12 @@ export async function isModuleActive(dleAddress, moduleId) {
|
|||||||
* @param {string} moduleId - ID модуля
|
* @param {string} moduleId - ID модуля
|
||||||
* @returns {Promise<string>} - Адрес модуля
|
* @returns {Promise<string>} - Адрес модуля
|
||||||
*/
|
*/
|
||||||
export async function getModuleAddress(dleAddress, moduleId) {
|
export async function getModuleAddress(dleAddress, moduleId, chainId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-modules/get-module-address', {
|
const response = await api.post('/dle-modules/get-module-address', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
moduleId: moduleId
|
moduleId: moduleId,
|
||||||
|
chainId: chainId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
@@ -338,7 +351,7 @@ export async function getModuleAddress(dleAddress, moduleId) {
|
|||||||
*/
|
*/
|
||||||
export async function isChainSupported(dleAddress, chainId) {
|
export async function isChainSupported(dleAddress, chainId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/is-chain-supported', {
|
const response = await api.post('/blockchain/is-chain-supported', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
chainId: chainId
|
chainId: chainId
|
||||||
});
|
});
|
||||||
@@ -361,7 +374,7 @@ export async function isChainSupported(dleAddress, chainId) {
|
|||||||
*/
|
*/
|
||||||
export async function getCurrentChainId(dleAddress) {
|
export async function getCurrentChainId(dleAddress) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/get-current-chain-id', {
|
const response = await api.post('/blockchain/get-current-chain-id', {
|
||||||
dleAddress: dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -384,7 +397,7 @@ export async function getCurrentChainId(dleAddress) {
|
|||||||
*/
|
*/
|
||||||
export async function checkProposalResult(dleAddress, proposalId) {
|
export async function checkProposalResult(dleAddress, proposalId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/blockchain/check-proposal-result', {
|
const response = await api.post('/blockchain/check-proposal-result', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
proposalId: proposalId
|
proposalId: proposalId
|
||||||
});
|
});
|
||||||
@@ -410,7 +423,7 @@ export async function checkProposalResult(dleAddress, proposalId) {
|
|||||||
*/
|
*/
|
||||||
export async function loadProposals(dleAddress) {
|
export async function loadProposals(dleAddress) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:8000/api/blockchain/get-proposals', {
|
const response = await api.post('/blockchain/get-proposals', {
|
||||||
dleAddress: dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -502,7 +515,7 @@ export async function loadAnalytics(dleAddress) {
|
|||||||
*/
|
*/
|
||||||
export async function getSupportedChains(dleAddress) {
|
export async function getSupportedChains(dleAddress) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:8000/api/blockchain/get-supported-chains', {
|
const response = await api.post('/blockchain/get-supported-chains', {
|
||||||
dleAddress: dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -676,7 +689,7 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support)
|
|||||||
*/
|
*/
|
||||||
export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:8000/api/blockchain/check-deactivation-proposal-result', {
|
const response = await api.post('/blockchain/check-deactivation-proposal-result', {
|
||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
proposalId: proposalId
|
proposalId: proposalId
|
||||||
});
|
});
|
||||||
@@ -738,7 +751,7 @@ export async function executeDeactivationProposal(dleAddress, proposalId) {
|
|||||||
*/
|
*/
|
||||||
export async function loadDeactivationProposals(dleAddress) {
|
export async function loadDeactivationProposals(dleAddress) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('http://localhost:8000/api/blockchain/load-deactivation-proposals', {
|
const response = await api.post('/blockchain/load-deactivation-proposals', {
|
||||||
dleAddress: dleAddress
|
dleAddress: dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket клиент для автоматического обновления данных
|
* WebSocket клиент для автоматического обновления данных
|
||||||
*/
|
*/
|
||||||
@@ -95,6 +107,23 @@ class WebSocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Алиас для on() - для совместимости с useDeploymentWebSocket
|
||||||
|
subscribe(event, callback) {
|
||||||
|
this.on(event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Алиас для off() - для совместимости с useDeploymentWebSocket
|
||||||
|
unsubscribe(event, callback) {
|
||||||
|
if (this.listeners.has(event)) {
|
||||||
|
const callbacks = this.listeners.get(event);
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
console.log(`[WebSocket] Отписались от события: ${event}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Отправка сообщения на сервер
|
// Отправка сообщения на сервер
|
||||||
send(event, data) {
|
send(event, data) {
|
||||||
if (this.ws && this.isConnected) {
|
if (this.ws && this.isConnected) {
|
||||||
|
|||||||
@@ -827,14 +827,42 @@
|
|||||||
|
|
||||||
<!-- Кнопка деплоя смарт-контрактов -->
|
<!-- Кнопка деплоя смарт-контрактов -->
|
||||||
<div class="deploy-section">
|
<div class="deploy-section">
|
||||||
|
<!-- Информация о поэтапном деплое -->
|
||||||
|
<div class="deployment-info">
|
||||||
|
<h4>🚀 Поэтапный деплой DLE</h4>
|
||||||
|
<p class="deployment-description">
|
||||||
|
Автоматический деплой DLE контракта и всех модулей с проверками, верификацией и инициализацией во всех выбранных сетях
|
||||||
|
</p>
|
||||||
|
<div class="deployment-features">
|
||||||
|
<div class="feature-item">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>Деплой DLE контракта во всех сетях</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>Автоматическая верификация контрактов</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>Деплой и инициализация всех модулей</span>
|
||||||
|
</div>
|
||||||
|
<div class="feature-item">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<span>Повторы при ошибках сети</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="deploy-buttons">
|
<div class="deploy-buttons">
|
||||||
<button
|
<button
|
||||||
@click="deploySmartContracts"
|
@click="deploySmartContracts"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-lg deploy-btn"
|
class="btn btn-primary btn-lg deploy-btn"
|
||||||
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
|
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
|
||||||
|
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
|
||||||
>
|
>
|
||||||
<i class="fas fa-rocket"></i> Деплой смарт контрактов
|
<i class="fas fa-cogs"></i>
|
||||||
|
Поэтапный деплой DLE
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="hasSelectedData"
|
v-if="hasSelectedData"
|
||||||
@@ -893,6 +921,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Мастер поэтапного деплоя -->
|
||||||
|
<div v-if="showDeploymentWizard" class="deployment-wizard-overlay">
|
||||||
|
<div class="wizard-container">
|
||||||
|
<DeploymentWizard
|
||||||
|
:private-key="unifiedPrivateKey"
|
||||||
|
:selected-networks="selectedNetworks"
|
||||||
|
:dle-data="dleSettings"
|
||||||
|
:etherscan-api-key="etherscanApiKey"
|
||||||
|
@deployment-completed="handleDeploymentCompleted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -900,10 +941,21 @@
|
|||||||
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useAuthContext } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import axios from 'axios';
|
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
|
import DeploymentWizard from '@/components/deployment/DeploymentWizard.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// Нормализация приватного ключа: убираем пробелы/"0x", посторонние символы,
|
||||||
|
// приводим к нижнему регистру и дополняем ведущими нулями до 64 символов
|
||||||
|
function normalizePrivateKey(raw) {
|
||||||
|
if (!raw || typeof raw !== 'string') return '';
|
||||||
|
let pk = raw.trim().replace(/^0x/i, '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||||
|
if (pk.length === 64) return '0x' + pk;
|
||||||
|
if (pk.length > 64) return '';
|
||||||
|
if (/^[0-9a-fA-F]*$/.test(pk)) return '0x' + pk.padStart(64, '0');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Получаем контекст авторизации для адреса кошелька
|
// Получаем контекст авторизации для адреса кошелька
|
||||||
const { address, isAdmin } = useAuthContext();
|
const { address, isAdmin } = useAuthContext();
|
||||||
@@ -995,6 +1047,10 @@ const autoVerifyAfterDeploy = ref(true);
|
|||||||
// Состояние для приватных ключей
|
// Состояние для приватных ключей
|
||||||
const useSameKeyForAllChains = ref(true);
|
const useSameKeyForAllChains = ref(true);
|
||||||
const unifiedPrivateKey = ref('');
|
const unifiedPrivateKey = ref('');
|
||||||
|
|
||||||
|
// Состояние мастера деплоя
|
||||||
|
const showDeploymentWizard = ref(false);
|
||||||
|
const deployedDLEAddress = ref('');
|
||||||
const privateKeys = reactive({});
|
const privateKeys = reactive({});
|
||||||
const privateKeyVisibility = reactive({});
|
const privateKeyVisibility = reactive({});
|
||||||
const keyValidation = reactive({});
|
const keyValidation = reactive({});
|
||||||
@@ -1060,7 +1116,6 @@ const hasSelectedNetworks = computed(() => {
|
|||||||
// symbol: dleSettings.tokenSymbol,
|
// symbol: dleSettings.tokenSymbol,
|
||||||
// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
|
// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
|
||||||
// };
|
// };
|
||||||
// const resp = await axios.post('/dle-v2/predict-addresses', payload);
|
|
||||||
// if (resp.data && resp.data.success && resp.data.data) {
|
// if (resp.data && resp.data.success && resp.data.data) {
|
||||||
// // ожидаем вид { [chainId]: address }
|
// // ожидаем вид { [chainId]: address }
|
||||||
// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
|
// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
|
||||||
@@ -1618,7 +1673,7 @@ const searchByPostalCode = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`[SearchByPostalCode] Querying Nominatim: ${params.toString()}`);
|
// console.log(`[SearchByPostalCode] Querying Nominatim: ${params.toString()}`);
|
||||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||||
|
|
||||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||||
// Преобразуем результаты Nominatim для отображения
|
// Преобразуем результаты Nominatim для отображения
|
||||||
@@ -1757,7 +1812,7 @@ const verifyAddress = async () => {
|
|||||||
params.append('countrycodes', 'RU');
|
params.append('countrycodes', 'RU');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||||
|
|
||||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||||
const verificationResult = response.data[0];
|
const verificationResult = response.data[0];
|
||||||
@@ -1833,7 +1888,7 @@ const formatTokenSymbol = () => {
|
|||||||
const loadCountries = async () => {
|
const loadCountries = async () => {
|
||||||
isLoadingCountries.value = true;
|
isLoadingCountries.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/countries');
|
const response = await api.get('/countries');
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
countriesOptions.value = response.data.data || [];
|
countriesOptions.value = response.data.data || [];
|
||||||
console.log(`Загружено стран: ${countriesOptions.value.length}`);
|
console.log(`Загружено стран: ${countriesOptions.value.length}`);
|
||||||
@@ -1857,7 +1912,7 @@ const loadRussianClassifiers = async () => {
|
|||||||
console.log('Загружаем российские классификаторы...');
|
console.log('Загружаем российские классификаторы...');
|
||||||
|
|
||||||
// Загружаем все классификаторы одним запросом для оптимизации
|
// Загружаем все классификаторы одним запросом для оптимизации
|
||||||
const response = await axios.get('/russian-classifiers/all');
|
const response = await api.get('/russian-classifiers/all');
|
||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
const data = response.data.data;
|
const data = response.data.data;
|
||||||
@@ -1905,7 +1960,7 @@ const loadKppCodes = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Загружаем КПП коды...');
|
console.log('Загружаем КПП коды...');
|
||||||
const response = await axios.get('/kpp/codes');
|
const response = await api.get('/kpp/codes');
|
||||||
|
|
||||||
if (response.data && Array.isArray(response.data.codes)) {
|
if (response.data && Array.isArray(response.data.codes)) {
|
||||||
kppCodes.value = response.data.codes;
|
kppCodes.value = response.data.codes;
|
||||||
@@ -1928,65 +1983,19 @@ const loadAvailableNetworks = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Загружаем доступные сети из базы данных...');
|
console.log('Загружаем доступные сети из базы данных...');
|
||||||
const response = await axios.get('/settings/rpc');
|
console.log('URL:', '/api/settings/rpc');
|
||||||
|
const response = await api.get('/settings/rpc');
|
||||||
|
console.log('Response:', response.data);
|
||||||
|
|
||||||
if (response.data && response.data.success) {
|
if (response.data && response.data.success) {
|
||||||
const networksData = response.data.data || [];
|
const networksData = response.data.data || [];
|
||||||
|
|
||||||
// Преобразуем данные из базы в формат для мульти-чейн деплоя
|
// Преобразуем данные из базы в формат для мульти-чейн деплоя
|
||||||
availableNetworks.value = networksData.map(network => {
|
availableNetworks.value = networksData.map(network => {
|
||||||
// Определяем примерную стоимость на основе chain_id
|
const chainId = network.chain_id || parseInt(network.network_id);
|
||||||
const estimatedCosts = {
|
const estimatedCost = getFallbackCost(chainId);
|
||||||
1: 45.50, // Ethereum Mainnet
|
const description = network.description || 'Блокчейн сеть';
|
||||||
137: 0.01, // Polygon
|
const name = network.name || network.network_id || `Chain ${chainId}`;
|
||||||
42161: 2.30, // Arbitrum One
|
|
||||||
10: 1.20, // Optimism
|
|
||||||
56: 0.50, // BSC
|
|
||||||
43114: 0.15, // Avalanche
|
|
||||||
11155111: 0.001, // Sepolia testnet
|
|
||||||
80001: 0.001, // Mumbai testnet
|
|
||||||
421613: 0.001, // Arbitrum Goerli
|
|
||||||
420: 0.001, // Optimism Goerli
|
|
||||||
97: 0.001, // BSC Testnet
|
|
||||||
43113: 0.001 // Avalanche Fuji
|
|
||||||
};
|
|
||||||
|
|
||||||
// Определяем описания сетей
|
|
||||||
const networkDescriptions = {
|
|
||||||
1: 'Максимальная безопасность и децентрализация',
|
|
||||||
137: 'Низкие комиссии, быстрые транзакции',
|
|
||||||
42161: 'Оптимистичные rollups, средние комиссии',
|
|
||||||
10: 'Оптимистичные rollups, низкие комиссии',
|
|
||||||
56: 'Совместимость с экосистемой Binance',
|
|
||||||
43114: 'Высокая пропускная способность',
|
|
||||||
11155111: 'Тестовая сеть Ethereum',
|
|
||||||
80001: 'Тестовая сеть Polygon',
|
|
||||||
421613: 'Тестовая сеть Arbitrum',
|
|
||||||
420: 'Тестовая сеть Optimism',
|
|
||||||
97: 'Тестовая сеть BSC',
|
|
||||||
43113: 'Тестовая сеть Avalanche'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Определяем названия сетей
|
|
||||||
const networkNames = {
|
|
||||||
1: 'Ethereum Mainnet',
|
|
||||||
137: 'Polygon',
|
|
||||||
42161: 'Arbitrum One',
|
|
||||||
10: 'Optimism',
|
|
||||||
56: 'BSC',
|
|
||||||
43114: 'Avalanche',
|
|
||||||
11155111: 'Sepolia Testnet',
|
|
||||||
80001: 'Mumbai Testnet',
|
|
||||||
421613: 'Arbitrum Goerli',
|
|
||||||
420: 'Optimism Goerli',
|
|
||||||
97: 'BSC Testnet',
|
|
||||||
43113: 'Avalanche Fuji'
|
|
||||||
};
|
|
||||||
|
|
||||||
const chainId = network.chain_id || parseInt(network.network_id);
|
|
||||||
const estimatedCost = estimatedCosts[chainId] || 1.00;
|
|
||||||
const description = networkDescriptions[chainId] || 'Блокчейн сеть';
|
|
||||||
const name = networkNames[chainId] || network.network_id || 'Unknown Network';
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chainId: chainId,
|
chainId: chainId,
|
||||||
@@ -2042,7 +2051,7 @@ const validateTokenStandardCompatibility = () => {
|
|||||||
|
|
||||||
// Проверяем совместимость ERC-4626 с тестовыми сетями
|
// Проверяем совместимость ERC-4626 с тестовыми сетями
|
||||||
if (standard === 'ERC4626') {
|
if (standard === 'ERC4626') {
|
||||||
const testnetChains = [11155111, 80001, 421613, 420, 97, 43113]; // Sepolia, Mumbai, etc.
|
const testnetChains = [11155111, 80001, 421613, 420, 97]; // Sepolia, Mumbai, etc.
|
||||||
const hasTestnet = networks.some(network => testnetChains.includes(network.chainId));
|
const hasTestnet = networks.some(network => testnetChains.includes(network.chainId));
|
||||||
|
|
||||||
if (hasTestnet) {
|
if (hasTestnet) {
|
||||||
@@ -2075,12 +2084,80 @@ const showTokenStandardWarnings = () => {
|
|||||||
|
|
||||||
// ==================== МУЛЬТИ-ЧЕЙН ФУНКЦИИ ====================
|
// ==================== МУЛЬТИ-ЧЕЙН ФУНКЦИИ ====================
|
||||||
|
|
||||||
// Обновление общей стоимости деплоя
|
// Обновление общей стоимости деплоя (динамический расчет)
|
||||||
const updateDeployCost = () => {
|
const updateDeployCost = async () => {
|
||||||
totalDeployCost.value = selectedNetworkDetails.value
|
if (selectedNetworkDetails.value.length === 0) {
|
||||||
.reduce((sum, network) => sum + network.estimatedCost, 0);
|
totalDeployCost.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Получаем chainId выбранных сетей
|
||||||
|
const chainIds = selectedNetworkDetails.value.map(network => network.chainId);
|
||||||
|
|
||||||
|
// Вызываем API для расчета стоимости
|
||||||
|
const response = await api.post('/dle-v2/estimate-cost', {
|
||||||
|
supportedChainIds: chainIds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
const costData = response.data.data;
|
||||||
|
|
||||||
|
// Обновляем информацию о каждой сети
|
||||||
|
selectedNetworkDetails.value.forEach(network => {
|
||||||
|
const estimate = costData.estimates.find(e => e.chainId === network.chainId);
|
||||||
|
|
||||||
|
if (estimate && estimate.ok) {
|
||||||
|
network.estimatedCost = parseFloat(estimate.costEth);
|
||||||
|
network.gasPrice = estimate.gasPrice;
|
||||||
|
network.estimatedGas = estimate.gasLimit;
|
||||||
|
} else {
|
||||||
|
// Fallback для сетей без RPC
|
||||||
|
network.estimatedCost = getFallbackCost(network.chainId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
totalDeployCost.value = parseFloat(costData.totalCostEth);
|
||||||
|
console.log('✅ Стоимость деплоя обновлена:', costData);
|
||||||
|
} else {
|
||||||
|
throw new Error('Ошибка получения стоимости деплоя');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Ошибка расчета стоимости, используем fallback:', error.message);
|
||||||
|
|
||||||
|
// Fallback к статическим ценам
|
||||||
|
selectedNetworkDetails.value.forEach(network => {
|
||||||
|
network.estimatedCost = getFallbackCost(network.chainId);
|
||||||
|
});
|
||||||
|
|
||||||
|
totalDeployCost.value = selectedNetworkDetails.value
|
||||||
|
.reduce((sum, network) => sum + network.estimatedCost, 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Вспомогательная функция для получения fallback стоимости
|
||||||
|
const getFallbackCost = (chainId) => {
|
||||||
|
const fallbackCosts = {
|
||||||
|
1: 45.50, // Ethereum Mainnet
|
||||||
|
137: 0.01, // Polygon
|
||||||
|
42161: 2.30, // Arbitrum One
|
||||||
|
10: 1.20, // Optimism
|
||||||
|
56: 0.50, // BSC
|
||||||
|
43114: 0.15, // Avalanche
|
||||||
|
11155111: 0.001, // Sepolia testnet
|
||||||
|
80001: 0.001, // Mumbai testnet
|
||||||
|
421613: 0.001, // Arbitrum Goerli
|
||||||
|
420: 0.001, // Optimism Goerli
|
||||||
|
97: 0.001, // BSC Testnet
|
||||||
|
17000: 0.001, // Holesky testnet
|
||||||
|
421614: 0.001, // Arbitrum Sepolia
|
||||||
|
84532: 0.001, // Base Sepolia
|
||||||
|
80002: 0.001 // Polygon Amoy
|
||||||
|
};
|
||||||
|
return fallbackCosts[chainId] || 1.00;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Копирование адреса DLE - отключено
|
// Копирование адреса DLE - отключено
|
||||||
// const copyAddress = async () => {
|
// const copyAddress = async () => {
|
||||||
// try {
|
// try {
|
||||||
@@ -2152,7 +2229,7 @@ const validatePrivateKey = async (chainId) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Отправляем запрос на бэкенд для валидации
|
// Отправляем запрос на бэкенд для валидации
|
||||||
const response = await axios.post('/dle-v2/validate-private-key', {
|
const response = await api.post('/dle-v2/validate-private-key', {
|
||||||
privateKey: key
|
privateKey: key
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2275,12 +2352,14 @@ const handleVisibilityChange = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watcher для unifiedPrivateKey с дебаунсом
|
// Watcher: нормализуем PK и обновляем связанные состояния
|
||||||
watch(unifiedPrivateKey, (newValue) => {
|
watch(unifiedPrivateKey, (newValue) => {
|
||||||
// Добавляем небольшую задержку для предотвращения рекурсии
|
const normalized = normalizePrivateKey(newValue);
|
||||||
setTimeout(() => {
|
if (normalized && normalized !== newValue) {
|
||||||
updateAllKeys();
|
unifiedPrivateKey.value = normalized;
|
||||||
}, 100);
|
return;
|
||||||
|
}
|
||||||
|
updateAllKeys();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watcher для predictedAddress - синхронизация с dleSettings - отключено
|
// Watcher для predictedAddress - синхронизация с dleSettings - отключено
|
||||||
@@ -2309,6 +2388,11 @@ watch(unifiedPrivateKey, (newValue) => {
|
|||||||
// Инициализация
|
// Инициализация
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
|
// Сбрасываем состояние деплоя при загрузке страницы
|
||||||
|
showDeployProgress.value = false;
|
||||||
|
deployProgress.value = 0;
|
||||||
|
deployStatus.value = '';
|
||||||
|
|
||||||
// Загружаем список стран
|
// Загружаем список стран
|
||||||
loadCountries();
|
loadCountries();
|
||||||
|
|
||||||
@@ -2337,6 +2421,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли приватный ключ
|
||||||
|
if (!unifiedPrivateKey.value) {
|
||||||
|
console.log('⚠️ Приватный ключ не введен. Пожалуйста, введите приватный ключ для деплоя.');
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем слушатель события видимости страницы для обновления списка сетей
|
// Добавляем слушатель события видимости страницы для обновления списка сетей
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
@@ -2367,23 +2456,22 @@ const checkAdminTokens = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
adminTokenCheck.value.isLoading = true;
|
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: true, error: null };
|
||||||
adminTokenCheck.value.error = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
|
const response = await api.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
adminTokenCheck.value.isAdmin = response.data.data.isAdmin;
|
adminTokenCheck.value = { ...adminTokenCheck.value, isAdmin: response.data.data.isAdmin };
|
||||||
console.log('Проверка админских токенов:', response.data.data);
|
console.log('Проверка админских токенов:', response.data.data);
|
||||||
} else {
|
} else {
|
||||||
adminTokenCheck.value.error = response.data.message || 'Ошибка проверки токенов';
|
adminTokenCheck.value = { ...adminTokenCheck.value, error: response.data.message || 'Ошибка проверки токенов' };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка проверки админских токенов:', error);
|
console.error('Ошибка проверки админских токенов:', error);
|
||||||
adminTokenCheck.value.error = error.response?.data?.message || 'Ошибка проверки токенов';
|
adminTokenCheck.value = { ...adminTokenCheck.value, error: error.response?.data?.message || 'Ошибка проверки токенов' };
|
||||||
} finally {
|
} finally {
|
||||||
adminTokenCheck.value.isLoading = false;
|
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2429,7 +2517,7 @@ const maskedPrivateKey = computed(() => {
|
|||||||
|
|
||||||
// Функция деплоя смарт-контрактов DLE
|
// Функция деплоя смарт-контрактов DLE
|
||||||
const deploySmartContracts = async () => {
|
const deploySmartContracts = async () => {
|
||||||
console.log('🚀 Начало деплоя DLE...');
|
console.log('🚀 Начало поэтапного деплоя DLE...');
|
||||||
try {
|
try {
|
||||||
// Валидация данных
|
// Валидация данных
|
||||||
if (!isFormValid.value) {
|
if (!isFormValid.value) {
|
||||||
@@ -2437,12 +2525,33 @@ const deploySmartContracts = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сразу показываем мастер деплоя
|
||||||
|
showDeploymentWizard.value = true;
|
||||||
|
|
||||||
|
// Запускаем деплой DLE в фоне
|
||||||
|
startStagedDeployment();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка деплоя DLE:', error);
|
||||||
|
showDeployProgress.value = false;
|
||||||
|
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция запуска поэтапного деплоя
|
||||||
|
const startStagedDeployment = async () => {
|
||||||
|
console.log('🚀 Запуск поэтапного деплоя...');
|
||||||
|
|
||||||
|
// Сначала выполняем стандартный деплой DLE контракта
|
||||||
|
try {
|
||||||
// Показываем индикатор процесса
|
// Показываем индикатор процесса
|
||||||
showDeployProgress.value = true;
|
showDeployProgress.value = true;
|
||||||
deployProgress.value = 10;
|
deployProgress.value = 10;
|
||||||
deployStatus.value = 'Подготовка данных для деплоя...';
|
deployStatus.value = 'Подготовка данных для деплоя DLE...';
|
||||||
|
|
||||||
// Подготовка данных для деплоя
|
// Подготовка данных для деплоя
|
||||||
|
console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks);
|
||||||
|
console.log('DEBUG: selectedNetworks.value:', selectedNetworks.value);
|
||||||
const deployData = {
|
const deployData = {
|
||||||
// Основная информация DLE
|
// Основная информация DLE
|
||||||
name: dleSettings.name,
|
name: dleSettings.name,
|
||||||
@@ -2463,16 +2572,15 @@ const deploySmartContracts = async () => {
|
|||||||
initialAmounts: dleSettings.partners.map(p => p.amount).filter(amount => amount > 0),
|
initialAmounts: dleSettings.partners.map(p => p.amount).filter(amount => amount > 0),
|
||||||
|
|
||||||
// Мульти-чейн настройки
|
// Мульти-чейн настройки
|
||||||
supportedChainIds: dleSettings.selectedNetworks || [],
|
supportedChainIds: selectedNetworks.value || [],
|
||||||
|
|
||||||
// Текущая цепочка (будет установлена при деплое)
|
// Текущая цепочка (будет установлена при деплое)
|
||||||
currentChainId: dleSettings.selectedNetworks[0] || 1,
|
currentChainId: selectedNetworks.value[0] || 1,
|
||||||
|
|
||||||
// Приватный ключ для деплоя
|
// Приватный ключ для деплоя
|
||||||
privateKey: unifiedPrivateKey.value,
|
privateKey: unifiedPrivateKey.value,
|
||||||
// Верификация через Etherscan V2
|
// Верификация через Etherscan V2
|
||||||
etherscanApiKey: etherscanApiKey.value,
|
etherscanApiKey: etherscanApiKey.value,
|
||||||
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
|
autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя
|
||||||
};
|
};
|
||||||
|
|
||||||
// Обработка логотипа
|
// Обработка логотипа
|
||||||
@@ -2480,7 +2588,7 @@ const deploySmartContracts = async () => {
|
|||||||
if (logoFile.value) {
|
if (logoFile.value) {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('logo', logoFile.value);
|
form.append('logo', logoFile.value);
|
||||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||||
if (uploaded) {
|
if (uploaded) {
|
||||||
deployData.logoURI = uploaded;
|
deployData.logoURI = uploaded;
|
||||||
@@ -2488,162 +2596,113 @@ const deploySmartContracts = async () => {
|
|||||||
} else if (ensResolvedUrl.value) {
|
} else if (ensResolvedUrl.value) {
|
||||||
deployData.logoURI = ensResolvedUrl.value;
|
deployData.logoURI = ensResolvedUrl.value;
|
||||||
} else {
|
} else {
|
||||||
// фолбэк на дефолт
|
|
||||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Ошибка при обработке логотипа:', error.message);
|
console.warn('Ошибка при обработке логотипа:', error.message);
|
||||||
// Используем fallback логотип
|
|
||||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Данные для деплоя DLE:', deployData);
|
console.log('Данные для деплоя DLE:', deployData);
|
||||||
|
|
||||||
// Предварительная проверка балансов во всех сетях
|
// Предварительная проверка балансов (через приватный ключ)
|
||||||
deployProgress.value = 20;
|
deployProgress.value = 20;
|
||||||
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
|
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
|
||||||
try {
|
try {
|
||||||
const pre = await axios.post('/dle-v2/precheck', {
|
const pre = await api.post('/dle-v2/precheck', {
|
||||||
supportedChainIds: deployData.supportedChainIds,
|
supportedChainIds: deployData.supportedChainIds,
|
||||||
privateKey: deployData.privateKey
|
privateKey: unifiedPrivateKey.value
|
||||||
});
|
});
|
||||||
const preData = pre.data?.data;
|
const preData = pre.data?.data;
|
||||||
if (pre.data?.success && preData) {
|
if (pre.data?.success && preData) {
|
||||||
const lacks = (preData.insufficient || []);
|
const lacks = (preData.insufficient || []);
|
||||||
const warnings = (preData.warnings || []);
|
|
||||||
|
|
||||||
if (lacks.length > 0) {
|
if (lacks.length > 0) {
|
||||||
const lines = (preData.balances || []).map(b => {
|
const message = `❌ Недостаточно средств в некоторых сетях!`;
|
||||||
const status = b.ok ? '✅' : '❌';
|
alert(message);
|
||||||
const warning = warnings.includes(b.chainId) ? ' ⚠️' : '';
|
showDeployProgress.value = false;
|
||||||
return `${status} Chain ${b.chainId}: ${b.balanceEth} ETH (мин. ${b.minRequiredEth} ETH)${warning}`;
|
return;
|
||||||
});
|
|
||||||
|
|
||||||
const message = `Проверка балансов завершена:\n\n${lines.join('\n')}\n\n${lacks.length > 0 ? '❌ Недостаточно средств в некоторых сетях!' : ''}\n${warnings.length > 0 ? '⚠️ Предупреждения в некоторых сетях!' : ''}`;
|
|
||||||
|
|
||||||
if (lacks.length > 0) {
|
|
||||||
alert(message);
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
return;
|
|
||||||
} else if (warnings.length > 0) {
|
|
||||||
const proceed = confirm(message + '\n\nПродолжить деплой?');
|
|
||||||
if (!proceed) {
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Проверка балансов пройдена:', preData.summary);
|
console.log('✅ Проверка балансов пройдена:', preData.summary);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('⚠️ Ошибка проверки балансов:', e.message);
|
console.warn('⚠️ Ошибка проверки балансов:', e.message);
|
||||||
// Если precheck недоступен, не блокируем — продолжаем
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deployProgress.value = 30;
|
deployProgress.value = 30;
|
||||||
deployStatus.value = 'Компиляция смарт-контрактов...';
|
deployStatus.value = 'Компиляция смарт-контрактов...';
|
||||||
|
|
||||||
// Автокомпиляция контрактов перед деплоем
|
// Автокомпиляция контрактов
|
||||||
console.log('🔨 Запуск автокомпиляции...');
|
|
||||||
try {
|
try {
|
||||||
const compileResponse = await axios.post('/compile-contracts');
|
const compileResponse = await api.post('/compile-contracts');
|
||||||
console.log('✅ Контракты скомпилированы:', compileResponse.data);
|
console.log('✅ Контракты скомпилированы:', compileResponse.data);
|
||||||
} catch (compileError) {
|
} catch (compileError) {
|
||||||
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
|
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
|
||||||
// Продолжаем деплой даже если компиляция не удалась
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deployProgress.value = 40;
|
deployProgress.value = 40;
|
||||||
deployStatus.value = 'Отправка данных на сервер...';
|
deployStatus.value = 'Деплой DLE контракта...';
|
||||||
|
|
||||||
// Вызов API для деплоя
|
// Деплой будет выполнен в DeploymentWizard
|
||||||
deployProgress.value = 50;
|
// Здесь только показываем мастер деплоя
|
||||||
deployStatus.value = 'Деплой смарт-контракта в блокчейне...';
|
|
||||||
|
|
||||||
const response = await axios.post('/dle-v2', deployData);
|
|
||||||
|
|
||||||
deployProgress.value = 80;
|
deployProgress.value = 80;
|
||||||
deployStatus.value = 'Проверка результатов деплоя...';
|
deployStatus.value = 'Запуск мастера деплоя...';
|
||||||
|
|
||||||
if (response.data.success) {
|
// Показываем мастер деплоя
|
||||||
const result = response.data.data;
|
showDeploymentWizard.value = true;
|
||||||
|
|
||||||
// Проверяем результаты мульти-чейн деплоя
|
|
||||||
if (result.networks && Array.isArray(result.networks)) {
|
|
||||||
const successfulNetworks = result.networks.filter(n => n.success);
|
|
||||||
const failedNetworks = result.networks.filter(n => !n.success);
|
|
||||||
|
|
||||||
if (failedNetworks.length > 0) {
|
|
||||||
console.warn('Некоторые сети не удалось развернуть:', failedNetworks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successfulNetworks.length > 0) {
|
|
||||||
// Проверяем, что все адреса одинаковые
|
|
||||||
const addresses = successfulNetworks.map(n => n.address);
|
|
||||||
const uniqueAddresses = [...new Set(addresses)];
|
|
||||||
|
|
||||||
if (uniqueAddresses.length === 1) {
|
|
||||||
deployProgress.value = 100;
|
|
||||||
deployStatus.value = `✅ DLE успешно развернут в ${successfulNetworks.length} сетях с одинаковым адресом!`;
|
|
||||||
|
|
||||||
console.log('🎉 Мульти-чейн деплой завершен успешно!');
|
|
||||||
console.log('Адрес DLE:', uniqueAddresses[0]);
|
|
||||||
console.log('Сети:', successfulNetworks.map(n => `Chain ${n.chainId}: ${n.address}`));
|
|
||||||
|
|
||||||
// Небольшая задержка для показа успешного завершения
|
|
||||||
setTimeout(() => {
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
// Перенаправляем на главную страницу управления
|
|
||||||
router.push('/management');
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
alert('❌ ОШИБКА: Адреса DLE в разных сетях не совпадают! Это может указывать на проблему с CREATE2.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
alert('❌ Не удалось развернуть DLE ни в одной сети');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback для одиночного деплоя
|
|
||||||
deployProgress.value = 100;
|
|
||||||
deployStatus.value = '✅ DLE успешно развернут!';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
router.push('/management');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
showDeployProgress.value = false;
|
|
||||||
alert('❌ Ошибка при деплое: ' + (response.data.message || response.data.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Мастер деплоя сам выполнит деплой
|
||||||
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка деплоя DLE:', error);
|
console.error('Ошибка при запуске деплоя:', error);
|
||||||
showDeployProgress.value = false;
|
deployStatus.value = `❌ Ошибка: ${error.message}`;
|
||||||
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
deployProgress.value = 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик завершения поэтапного деплоя
|
||||||
|
const handleDeploymentCompleted = (result) => {
|
||||||
|
console.log('🎉 Поэтапный деплой завершен:', result);
|
||||||
|
showDeploymentWizard.value = false;
|
||||||
|
|
||||||
|
// Перенаправляем на главную страницу управления
|
||||||
|
router.push('/management');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Валидация формы
|
// Валидация формы
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
|
const validation = {
|
||||||
|
jurisdiction: !!dleSettings.jurisdiction,
|
||||||
|
name: !!dleSettings.name,
|
||||||
|
tokenSymbol: !!dleSettings.tokenSymbol,
|
||||||
|
partners: dleSettings.partners.length > 0,
|
||||||
|
partnersValid: dleSettings.partners.every(partner => partner.address && partner.amount > 0),
|
||||||
|
quorum: dleSettings.governanceQuorum > 0 && dleSettings.governanceQuorum <= 100,
|
||||||
|
networks: selectedNetworks.value.length > 0,
|
||||||
|
privateKey: !!unifiedPrivateKey.value,
|
||||||
|
keyValid: !!keyValidation.unified?.isValid,
|
||||||
|
coordinates: validateCoordinates(dleSettings.coordinates)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🔍 Валидация формы:', validation);
|
||||||
|
console.log('🔍 selectedNetworks.value:', selectedNetworks.value);
|
||||||
|
console.log('🔍 adminTokenCheck:', adminTokenCheck.value);
|
||||||
|
console.log('🔍 showDeployProgress:', showDeployProgress.value);
|
||||||
|
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
|
||||||
|
console.log('🔍 keyValidation.unified:', keyValidation.unified);
|
||||||
|
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates);
|
||||||
|
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading && !showDeployProgress.value);
|
||||||
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
dleSettings.jurisdiction &&
|
validation.jurisdiction &&
|
||||||
dleSettings.name &&
|
validation.name &&
|
||||||
dleSettings.tokenSymbol &&
|
validation.tokenSymbol &&
|
||||||
(dleSettings.partners.length > 0) &&
|
validation.partners &&
|
||||||
dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
|
validation.partnersValid &&
|
||||||
dleSettings.governanceQuorum > 0 &&
|
validation.quorum &&
|
||||||
dleSettings.governanceQuorum <= 100 &&
|
validation.networks &&
|
||||||
(dleSettings.selectedNetworks.length > 0) &&
|
validation.privateKey &&
|
||||||
// Проверка приватного ключа
|
validation.keyValid &&
|
||||||
unifiedPrivateKey.value &&
|
validation.coordinates
|
||||||
keyValidation.unified?.isValid &&
|
|
||||||
// Валидация координат
|
|
||||||
validateCoordinates(dleSettings.coordinates)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2715,7 +2774,7 @@ async function submitDeploy() {
|
|||||||
if (logoFile.value) {
|
if (logoFile.value) {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('logo', logoFile.value);
|
form.append('logo', logoFile.value);
|
||||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||||
if (uploaded) {
|
if (uploaded) {
|
||||||
deployData.logoURI = uploaded;
|
deployData.logoURI = uploaded;
|
||||||
@@ -4385,6 +4444,85 @@ async function submitDeploy() {
|
|||||||
border-top: 1px solid #e9ecef;
|
border-top: 1px solid #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для информации о деплое */
|
||||||
|
.deployment-info {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 2rem;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-info h4 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-description {
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item i {
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-item span {
|
||||||
|
color: #495057;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для мастера деплоя */
|
||||||
|
.deployment-wizard-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wizard-container {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.deploy-buttons {
|
.deploy-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<div class="proposal-header">
|
<div class="proposal-header">
|
||||||
<h5>{{ proposal.description || 'Без описания' }}</h5>
|
<h5>{{ getProposalTitle(proposal) }}</h5>
|
||||||
<span class="proposal-status" :class="proposal.status">
|
<span class="proposal-status" :class="proposal.status">
|
||||||
{{ getProposalStatusText(proposal.status) }}
|
{{ getProposalStatusText(proposal.status) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -92,6 +92,27 @@
|
|||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
|
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Детальная информация о модуле -->
|
||||||
|
<div v-if="proposal.decodedData" class="module-details">
|
||||||
|
<div class="detail-item">
|
||||||
|
<strong>Тип модуля:</strong> {{ getModuleName(proposal.decodedData.moduleId) }}
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<strong>Адрес модуля:</strong>
|
||||||
|
<a :href="getEtherscanUrl(proposal.decodedData.moduleAddress, proposal.decodedData.chainId)"
|
||||||
|
target="_blank" class="address-link">
|
||||||
|
{{ shortenAddress(proposal.decodedData.moduleAddress) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<strong>Сеть:</strong> {{ getChainName(proposal.decodedData.chainId) }}
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<strong>Длительность:</strong> {{ formatDuration(proposal.decodedData.duration) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<strong>Голоса:</strong>
|
<strong>Голоса:</strong>
|
||||||
<div class="votes-container">
|
<div class="votes-container">
|
||||||
@@ -100,7 +121,7 @@
|
|||||||
<span class="against">Против: {{ formatVotes(proposal.againstVotes) }}</span>
|
<span class="against">Против: {{ formatVotes(proposal.againstVotes) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="quorum-info">
|
<div class="quorum-info">
|
||||||
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum() }}%</span>
|
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum(proposal) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="quorum-progress">
|
<div class="quorum-progress">
|
||||||
<div class="progress-bar">
|
<div class="progress-bar">
|
||||||
@@ -140,13 +161,21 @@
|
|||||||
<i class="fas fa-times"></i> Против
|
<i class="fas fa-times"></i> Против
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canExecute(proposal) && props.isAuthenticated && hasAdminRights()"
|
v-if="canExecute(proposal) && props.isAuthenticated"
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
@click="executeProposalLocal(proposal.id)"
|
@click="executeProposalLocal(proposal.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-play"></i> Исполнить
|
<i class="fas fa-play"></i> Исполнить
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Информация для не-инициаторов -->
|
||||||
|
<div v-else-if="proposal.state === 5 && !proposal.executed && props.isAuthenticated" class="execution-notice">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Только инициатор предложения может его исполнить
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Информация для неавторизованных пользователей -->
|
<!-- Информация для неавторизованных пользователей -->
|
||||||
<div v-if="!props.isAuthenticated" class="auth-notice">
|
<div v-if="!props.isAuthenticated" class="auth-notice">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
@@ -533,7 +562,7 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import { useAuthContext } from '../../composables/useAuth';
|
import { useAuthContext } from '../../composables/useAuth';
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
|
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
|
||||||
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI } from '../../services/proposalsService.js';
|
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
|
||||||
import api from '../../api/axios';
|
import api from '../../api/axios';
|
||||||
const showTargetChains = computed(() => {
|
const showTargetChains = computed(() => {
|
||||||
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
|
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
|
||||||
@@ -543,6 +572,306 @@ const showTargetChains = computed(() => {
|
|||||||
import wsClient from '../../utils/websocket.js';
|
import wsClient from '../../utils/websocket.js';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
|
// Best Practice: WebSocket-based подписка на обновления голосования
|
||||||
|
function subscribeToVoteUpdates(txHash, proposalId, actionType) {
|
||||||
|
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для:', { txHash, proposalId, actionType });
|
||||||
|
|
||||||
|
// Создаем уникальный обработчик для этой транзакции
|
||||||
|
const voteHandler = (data) => {
|
||||||
|
console.log('[DleProposalsView] Получено WebSocket уведомление о голосовании:', data);
|
||||||
|
|
||||||
|
// Проверяем, что это наша транзакция
|
||||||
|
if (data.txHash === txHash || data.proposalId === proposalId) {
|
||||||
|
console.log('[DleProposalsView] Найдено совпадение транзакции, обновляем данные');
|
||||||
|
|
||||||
|
// Обновляем данные
|
||||||
|
loadDleData().then(() => {
|
||||||
|
// Показываем успешное уведомление
|
||||||
|
showSuccessNotification(txHash, actionType);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отписываемся от уведомлений
|
||||||
|
wsClient.off('proposal_voted', voteHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Подписываемся на уведомления о голосовании
|
||||||
|
wsClient.on('proposal_voted', voteHandler);
|
||||||
|
|
||||||
|
// Устанавливаем таймаут на случай, если WebSocket не сработает
|
||||||
|
setTimeout(() => {
|
||||||
|
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений, отписываемся');
|
||||||
|
wsClient.off('proposal_voted', voteHandler);
|
||||||
|
|
||||||
|
// Fallback: обновляем данные в любом случае
|
||||||
|
loadDleData().then(() => {
|
||||||
|
showTimeoutNotification(txHash, actionType);
|
||||||
|
});
|
||||||
|
}, 60000); // 60 секунд таймаут
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket-based подписка на обновления исполнения
|
||||||
|
function subscribeToExecutionUpdates(txHash, proposalId) {
|
||||||
|
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для исполнения:', { txHash, proposalId });
|
||||||
|
|
||||||
|
// Создаем уникальный обработчик для этой транзакции
|
||||||
|
const executionHandler = (data) => {
|
||||||
|
console.log('[DleProposalsView] Получено WebSocket уведомление об исполнении:', data);
|
||||||
|
|
||||||
|
// Проверяем, что это наша транзакция
|
||||||
|
if (data.txHash === txHash || data.proposalId === proposalId) {
|
||||||
|
console.log('[DleProposalsView] Найдено совпадение транзакции исполнения, обновляем данные');
|
||||||
|
|
||||||
|
// Обновляем данные
|
||||||
|
loadDleData().then(() => {
|
||||||
|
// Показываем успешное уведомление
|
||||||
|
showSuccessNotification(txHash, 'execution');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отписываемся от уведомлений
|
||||||
|
wsClient.off('proposal_executed', executionHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Подписываемся на уведомления об исполнении
|
||||||
|
wsClient.on('proposal_executed', executionHandler);
|
||||||
|
|
||||||
|
// Устанавливаем таймаут на случай, если WebSocket не сработает
|
||||||
|
setTimeout(() => {
|
||||||
|
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений об исполнении, отписываемся');
|
||||||
|
wsClient.off('proposal_executed', executionHandler);
|
||||||
|
|
||||||
|
// Fallback: обновляем данные в любом случае
|
||||||
|
loadDleData().then(() => {
|
||||||
|
showTimeoutNotification(txHash, 'execution');
|
||||||
|
});
|
||||||
|
}, 60000); // 60 секунд таймаут
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для отслеживания транзакции исполнения на backend
|
||||||
|
async function trackExecutionTransaction(txHash, dleAddress, proposalId) {
|
||||||
|
try {
|
||||||
|
console.log('[DleProposalsView] Запускаем отслеживание транзакции исполнения на backend:', { txHash, dleAddress, proposalId });
|
||||||
|
|
||||||
|
const response = await api.post('/dle-proposals/track-execution-transaction', {
|
||||||
|
txHash: txHash,
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('[DleProposalsView] Backend подтвердил транзакцию исполнения:', response.data);
|
||||||
|
} else {
|
||||||
|
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию исполнения:', response.data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DleProposalsView] Ошибка при отслеживании транзакции исполнения на backend:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для отслеживания транзакции голосования на backend
|
||||||
|
async function trackVoteTransaction(txHash, dleAddress, proposalId, support) {
|
||||||
|
try {
|
||||||
|
console.log('[DleProposalsView] Запускаем отслеживание транзакции на backend:', { txHash, dleAddress, proposalId, support });
|
||||||
|
|
||||||
|
const response = await api.post('/dle-proposals/track-vote-transaction', {
|
||||||
|
txHash: txHash,
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId,
|
||||||
|
support: support
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('[DleProposalsView] Backend подтвердил транзакцию:', response.data);
|
||||||
|
} else {
|
||||||
|
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию:', response.data.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DleProposalsView] Ошибка при отслеживании транзакции на backend:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показ уведомления о транзакции
|
||||||
|
function showTransactionNotification(txHash, message) {
|
||||||
|
// Создаем уведомление с ссылкой на Etherscan
|
||||||
|
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||||
|
|
||||||
|
// Можно использовать toast-библиотеку или создать кастомное уведомление
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'transaction-notification';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-header">
|
||||||
|
<span class="notification-icon">⏳</span>
|
||||||
|
<span class="notification-title">${message}</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-body">
|
||||||
|
<p>Ожидаем подтверждения транзакции...</p>
|
||||||
|
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||||
|
Посмотреть в Etherscan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Добавляем стили
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Автоматически удаляем через 10 секунд
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показ успешного уведомления
|
||||||
|
function showSuccessNotification(txHash, actionType) {
|
||||||
|
const actionText = actionType === 'vote' ? 'Голосование подтверждено!' : 'Голосование "против" подтверждено!';
|
||||||
|
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'success-notification';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-header">
|
||||||
|
<span class="notification-icon">✅</span>
|
||||||
|
<span class="notification-title">${actionText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-body">
|
||||||
|
<p>Данные обновлены</p>
|
||||||
|
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||||
|
Посмотреть в Etherscan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показ уведомления об ошибке
|
||||||
|
function showErrorNotification(txHash, message) {
|
||||||
|
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'error-notification';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-header">
|
||||||
|
<span class="notification-icon">❌</span>
|
||||||
|
<span class="notification-title">${message}</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-body">
|
||||||
|
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||||
|
Посмотреть в Etherscan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показ уведомления о таймауте
|
||||||
|
function showTimeoutNotification(txHash, actionType) {
|
||||||
|
const actionText = actionType === 'vote' ? 'Голосование' : 'Голосование "против"';
|
||||||
|
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'timeout-notification';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-header">
|
||||||
|
<span class="notification-icon">⏰</span>
|
||||||
|
<span class="notification-title">${actionText} отправлено</span>
|
||||||
|
</div>
|
||||||
|
<div class="notification-body">
|
||||||
|
<p>Подтверждение не получено, но данные обновлены</p>
|
||||||
|
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||||
|
Посмотреть в Etherscan
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
dleAddress: { type: String, required: false, default: null },
|
dleAddress: { type: String, required: false, default: null },
|
||||||
dleContract: { type: Object, required: false, default: null },
|
dleContract: { type: Object, required: false, default: null },
|
||||||
@@ -670,15 +999,30 @@ async function loadDleData() {
|
|||||||
console.log('[Frontend] Массив предложений:', proposalsData);
|
console.log('[Frontend] Массив предложений:', proposalsData);
|
||||||
|
|
||||||
// Преобразуем данные из API в формат для frontend
|
// Преобразуем данные из API в формат для frontend
|
||||||
proposals.value = proposalsData.map(proposal => {
|
proposals.value = await Promise.all(proposalsData.map(async (proposal) => {
|
||||||
const transformedProposal = {
|
const transformedProposal = {
|
||||||
...proposal,
|
...proposal,
|
||||||
status: getProposalStatus(proposal),
|
status: getProposalStatus(proposal),
|
||||||
deadline: proposal.deadline || 0
|
deadline: proposal.deadline || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Если есть transactionHash, декодируем данные предложения
|
||||||
|
if (proposal.transactionHash) {
|
||||||
|
try {
|
||||||
|
console.log('[Frontend] Декодируем данные предложения:', proposal.transactionHash);
|
||||||
|
const decodedData = await decodeProposalData(proposal.transactionHash);
|
||||||
|
if (decodedData.success) {
|
||||||
|
transformedProposal.decodedData = decodedData.data;
|
||||||
|
console.log('[Frontend] Декодированные данные:', decodedData.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Frontend] Ошибка декодирования данных предложения:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[Frontend] Преобразованное предложение:', transformedProposal);
|
console.log('[Frontend] Преобразованное предложение:', transformedProposal);
|
||||||
return transformedProposal;
|
return transformedProposal;
|
||||||
});
|
}));
|
||||||
|
|
||||||
console.log('[Frontend] Итоговый список предложений:', proposals.value);
|
console.log('[Frontend] Итоговый список предложений:', proposals.value);
|
||||||
|
|
||||||
@@ -819,8 +1163,13 @@ function getProposalStatus(proposal) {
|
|||||||
const forVotes = Number(proposal.forVotes) || 0;
|
const forVotes = Number(proposal.forVotes) || 0;
|
||||||
const againstVotes = Number(proposal.againstVotes) || 0;
|
const againstVotes = Number(proposal.againstVotes) || 0;
|
||||||
|
|
||||||
// Если есть голоса, определяем результат
|
// Проверяем, достигнут ли кворум
|
||||||
if (forVotes > 0 || againstVotes > 0) {
|
const quorumPercentage = getQuorumPercentage(proposal);
|
||||||
|
const requiredQuorum = getRequiredQuorum(proposal);
|
||||||
|
const quorumReached = quorumPercentage >= requiredQuorum;
|
||||||
|
|
||||||
|
// Если есть голоса И кворум достигнут, определяем результат
|
||||||
|
if ((forVotes > 0 || againstVotes > 0) && quorumReached) {
|
||||||
if (forVotes > againstVotes) {
|
if (forVotes > againstVotes) {
|
||||||
return 'succeeded';
|
return 'succeeded';
|
||||||
} else if (againstVotes > forVotes) {
|
} else if (againstVotes > forVotes) {
|
||||||
@@ -828,6 +1177,7 @@ function getProposalStatus(proposal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если кворум не достигнут или нет голосов, предложение активно
|
||||||
return 'active';
|
return 'active';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,6 +1193,61 @@ function getProposalStatusText(status) {
|
|||||||
return statusMap[status] || status;
|
return statusMap[status] || status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProposalTitle(proposal) {
|
||||||
|
// Если есть декодированные данные, показываем детальную информацию
|
||||||
|
if (proposal.decodedData) {
|
||||||
|
const { moduleId, moduleAddress, chainId, duration } = proposal.decodedData;
|
||||||
|
|
||||||
|
// Декодируем moduleId из hex в строку
|
||||||
|
let moduleName = 'Неизвестный модуль';
|
||||||
|
try {
|
||||||
|
moduleName = ethers.toUtf8String(moduleId).replace(/\0/g, '');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Не удалось декодировать moduleId:', moduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Добавить модуль: ${moduleName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Иначе показываем обычное описание
|
||||||
|
return proposal.description || 'Без описания';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModuleName(moduleId) {
|
||||||
|
try {
|
||||||
|
return ethers.toUtf8String(moduleId).replace(/\0/g, '');
|
||||||
|
} catch (e) {
|
||||||
|
return 'Неизвестный модуль';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEtherscanUrl(address, chainId) {
|
||||||
|
const chainMap = {
|
||||||
|
1: 'https://etherscan.io',
|
||||||
|
11155111: 'https://sepolia.etherscan.io',
|
||||||
|
17000: 'https://holesky.etherscan.io',
|
||||||
|
421614: 'https://sepolia.arbiscan.io',
|
||||||
|
84532: 'https://sepolia.basescan.org'
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = chainMap[chainId] || 'https://etherscan.io';
|
||||||
|
return `${baseUrl}/address/${address}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days} дн. ${hours} ч.`;
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours} ч. ${minutes} мин.`;
|
||||||
|
} else {
|
||||||
|
return `${minutes} мин.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getProposalStatusClass(status) {
|
function getProposalStatusClass(status) {
|
||||||
const classMap = {
|
const classMap = {
|
||||||
'pending': 'status-pending',
|
'pending': 'status-pending',
|
||||||
@@ -901,16 +1306,25 @@ function getQuorumPercentage(proposal) {
|
|||||||
|
|
||||||
function getQuorumProgress(proposal) {
|
function getQuorumProgress(proposal) {
|
||||||
const percentage = getQuorumPercentage(proposal);
|
const percentage = getQuorumPercentage(proposal);
|
||||||
const requiredQuorum = getRequiredQuorum();
|
const requiredQuorum = getRequiredQuorum(proposal);
|
||||||
const progress = Math.min((percentage / requiredQuorum) * 100, 100);
|
const progress = Math.min((percentage / requiredQuorum) * 100, 100);
|
||||||
console.log('[Quorum] Прогресс кворума:', { percentage, requiredQuorum, progress });
|
console.log('[Quorum] Прогресс кворума:', { percentage, requiredQuorum, progress });
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRequiredQuorum() {
|
function getRequiredQuorum(proposal = null) {
|
||||||
|
// Если есть данные о предложении с quorumRequired, используем их
|
||||||
|
if (proposal && proposal.quorumRequired && selectedDle.value?.totalSupply) {
|
||||||
|
const totalSupplyWei = parseFloat(selectedDle.value.totalSupply) * Math.pow(10, 18);
|
||||||
|
const quorumPercentage = (proposal.quorumRequired / totalSupplyWei) * 100;
|
||||||
|
console.log('[Quorum] Требуемый кворум из предложения:', quorumPercentage, 'quorumRequired:', proposal.quorumRequired, 'totalSupply:', totalSupplyWei);
|
||||||
|
return Math.round(quorumPercentage * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback к данным DLE
|
||||||
const quorum = selectedDle.value?.quorumPercentage || 51;
|
const quorum = selectedDle.value?.quorumPercentage || 51;
|
||||||
console.log('[Quorum] Требуемый кворум из DLE:', quorum, 'DLE данные:', selectedDle.value);
|
console.log('[Quorum] Требуемый кворум из DLE:', quorum, 'DLE данные:', selectedDle.value);
|
||||||
return quorum; // По умолчанию 51% если данные не загружены
|
return quorum;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatVotes(votes) {
|
function formatVotes(votes) {
|
||||||
@@ -974,15 +1388,17 @@ function canExecute(proposal) {
|
|||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const deadline = proposal.deadline || 0;
|
const deadline = proposal.deadline || 0;
|
||||||
|
|
||||||
// Предложение можно выполнить только если:
|
// Предложение можно выполнить если:
|
||||||
// 1. Дедлайн истек
|
// 1. Кворум достигнут ИЛИ предложение уже принято (state: 5)
|
||||||
// 2. Кворум достигнут
|
// 2. Предложение еще не выполнено
|
||||||
// 3. Предложение еще не выполнено
|
|
||||||
const quorumPercentage = getQuorumPercentage(proposal);
|
const quorumPercentage = getQuorumPercentage(proposal);
|
||||||
const requiredQuorum = getRequiredQuorum();
|
const requiredQuorum = getRequiredQuorum(proposal);
|
||||||
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
|
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
|
||||||
const deadlinePassed = deadline > 0 && now >= deadline;
|
const deadlinePassed = deadline > 0 && now >= deadline;
|
||||||
|
|
||||||
|
// Если предложение уже принято (state: 5), можно исполнять
|
||||||
|
const isProposalPassed = proposal.state === 5 || proposal.isPassed === true;
|
||||||
|
|
||||||
// Добавляем отладочную информацию
|
// Добавляем отладочную информацию
|
||||||
console.log('[canExecute] Проверка предложения:', {
|
console.log('[canExecute] Проверка предложения:', {
|
||||||
proposalId: proposal.id,
|
proposalId: proposal.id,
|
||||||
@@ -992,10 +1408,29 @@ function canExecute(proposal) {
|
|||||||
deadline,
|
deadline,
|
||||||
now,
|
now,
|
||||||
deadlinePassed,
|
deadlinePassed,
|
||||||
executed: proposal.executed
|
executed: proposal.executed,
|
||||||
|
state: proposal.state,
|
||||||
|
isPassed: proposal.isPassed,
|
||||||
|
isProposalPassed
|
||||||
});
|
});
|
||||||
|
|
||||||
return deadlinePassed && hasReachedQuorum && !proposal.executed;
|
// Проверяем, что текущий пользователь - инициатор предложения
|
||||||
|
const isInitiator = address.value && proposal.initiator &&
|
||||||
|
address.value.toLowerCase() === proposal.initiator.toLowerCase();
|
||||||
|
|
||||||
|
console.log('[canExecute] Проверка инициатора:', {
|
||||||
|
currentAddress: address.value,
|
||||||
|
proposalInitiator: proposal.initiator,
|
||||||
|
isInitiator
|
||||||
|
});
|
||||||
|
|
||||||
|
// Можно исполнять если:
|
||||||
|
// 1. (кворум достигнут И дедлайн истек) ИЛИ предложение уже принято
|
||||||
|
// 2. Пользователь - инициатор предложения
|
||||||
|
// 3. Предложение не выполнено
|
||||||
|
return ((hasReachedQuorum && deadlinePassed) || isProposalPassed) &&
|
||||||
|
isInitiator &&
|
||||||
|
!proposal.executed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSigned(proposalId) {
|
function hasSigned(proposalId) {
|
||||||
@@ -1191,10 +1626,107 @@ async function signProposalLocal(proposalId) {
|
|||||||
console.log('[Debug] Попытка подписи для предложения:', proposalId);
|
console.log('[Debug] Попытка подписи для предложения:', proposalId);
|
||||||
console.log('[Debug] Адрес кошелька:', address.value);
|
console.log('[Debug] Адрес кошелька:', address.value);
|
||||||
|
|
||||||
await voteForProposalAPI(dleAddress.value, proposalId, true); // Подпись = голос "за"
|
// Получаем данные транзакции от backend
|
||||||
|
const result = await voteForProposalAPI(dleAddress.value, proposalId, true);
|
||||||
|
|
||||||
await loadDleData();
|
if (result.success) {
|
||||||
alert('✅ Предложение подписано!');
|
console.log('[DleProposalsView] Данные транзакции голосования получены:', result);
|
||||||
|
|
||||||
|
// Отправляем транзакцию через MetaMask
|
||||||
|
try {
|
||||||
|
// Проверяем валидность адреса
|
||||||
|
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||||
|
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что есть подключенный аккаунт
|
||||||
|
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
|
||||||
|
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
|
||||||
|
|
||||||
|
// Проверяем подключение к правильной сети
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
const expectedChainId = '0xaa36a7'; // Sepolia
|
||||||
|
|
||||||
|
if (chainId !== expectedChainId) {
|
||||||
|
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Пытаемся переключиться на Sepolia
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: expectedChainId }],
|
||||||
|
});
|
||||||
|
console.log('[DleProposalsView] Успешно переключились на Sepolia');
|
||||||
|
} catch (switchError) {
|
||||||
|
// Если сеть не добавлена, добавляем её
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
console.log('[DleProposalsView] Добавляем Sepolia сеть');
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: expectedChainId,
|
||||||
|
chainName: 'Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'SepoliaETH',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Отправляем транзакцию голосования:', {
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
});
|
||||||
|
|
||||||
|
const txHash = await window.ethereum.request({
|
||||||
|
method: 'eth_sendTransaction',
|
||||||
|
params: [{
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Транзакция голосования отправлена:', txHash);
|
||||||
|
|
||||||
|
// Показываем уведомление с возможностью отслеживания
|
||||||
|
showTransactionNotification(txHash, 'Голосование отправлено!');
|
||||||
|
|
||||||
|
// Подписываемся на WebSocket уведомления о голосовании
|
||||||
|
subscribeToVoteUpdates(txHash, proposalId, 'vote');
|
||||||
|
|
||||||
|
// Запускаем отслеживание транзакции на backend
|
||||||
|
trackVoteTransaction(txHash, dleAddress.value, proposalId, true);
|
||||||
|
|
||||||
|
} catch (txError) {
|
||||||
|
console.error('[DleProposalsView] Ошибка отправки транзакции голосования:', txError);
|
||||||
|
alert('❌ Ошибка отправки транзакции голосования: ' + txError.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при подписании:', error);
|
console.error('Ошибка при подписании:', error);
|
||||||
@@ -1225,10 +1757,107 @@ async function cancelSignatureLocal(proposalId) {
|
|||||||
console.log('[Debug] Попытка голосования "против" для предложения:', proposalId);
|
console.log('[Debug] Попытка голосования "против" для предложения:', proposalId);
|
||||||
console.log('[Debug] Адрес кошелька:', address.value);
|
console.log('[Debug] Адрес кошелька:', address.value);
|
||||||
|
|
||||||
await voteForProposalAPI(dleAddress.value, proposalId, false); // Голос "против"
|
// Получаем данные транзакции от backend
|
||||||
|
const result = await voteForProposalAPI(dleAddress.value, proposalId, false);
|
||||||
|
|
||||||
await loadDleData();
|
if (result.success) {
|
||||||
alert('✅ Ваш голос "против" учтен!');
|
console.log('[DleProposalsView] Данные транзакции голосования "против" получены:', result);
|
||||||
|
|
||||||
|
// Отправляем транзакцию через MetaMask
|
||||||
|
try {
|
||||||
|
// Проверяем валидность адреса
|
||||||
|
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||||
|
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что есть подключенный аккаунт
|
||||||
|
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
|
||||||
|
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
|
||||||
|
|
||||||
|
// Проверяем подключение к правильной сети
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
const expectedChainId = '0xaa36a7'; // Sepolia
|
||||||
|
|
||||||
|
if (chainId !== expectedChainId) {
|
||||||
|
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Пытаемся переключиться на Sepolia
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: expectedChainId }],
|
||||||
|
});
|
||||||
|
console.log('[DleProposalsView] Успешно переключились на Sepolia');
|
||||||
|
} catch (switchError) {
|
||||||
|
// Если сеть не добавлена, добавляем её
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
console.log('[DleProposalsView] Добавляем Sepolia сеть');
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: expectedChainId,
|
||||||
|
chainName: 'Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'SepoliaETH',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Отправляем транзакцию голосования "против":', {
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
});
|
||||||
|
|
||||||
|
const txHash = await window.ethereum.request({
|
||||||
|
method: 'eth_sendTransaction',
|
||||||
|
params: [{
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Транзакция голосования "против" отправлена:', txHash);
|
||||||
|
|
||||||
|
// Показываем уведомление с возможностью отслеживания
|
||||||
|
showTransactionNotification(txHash, 'Голосование "против" отправлено!');
|
||||||
|
|
||||||
|
// Подписываемся на WebSocket уведомления о голосовании
|
||||||
|
subscribeToVoteUpdates(txHash, proposalId, 'vote-against');
|
||||||
|
|
||||||
|
// Запускаем отслеживание транзакции на backend
|
||||||
|
trackVoteTransaction(txHash, dleAddress.value, proposalId, false);
|
||||||
|
|
||||||
|
} catch (txError) {
|
||||||
|
console.error('[DleProposalsView] Ошибка отправки транзакции голосования "против":', txError);
|
||||||
|
alert('❌ Ошибка отправки транзакции голосования "против": ' + txError.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при голосовании "против":', error);
|
console.error('Ошибка при голосовании "против":', error);
|
||||||
@@ -1243,23 +1872,131 @@ async function cancelSignatureLocal(proposalId) {
|
|||||||
|
|
||||||
// Исполнение предложения
|
// Исполнение предложения
|
||||||
async function executeProposalLocal(proposalId) {
|
async function executeProposalLocal(proposalId) {
|
||||||
// Проверка прав админа для исполнения
|
// Проверка авторизации
|
||||||
if (!props.isAuthenticated) {
|
if (!props.isAuthenticated) {
|
||||||
alert('❌ Для исполнения предложений необходимо авторизоваться в приложении');
|
alert('❌ Для исполнения предложений необходимо авторизоваться в приложении');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Дополнительная проверка на права админа
|
// Проверка, что пользователь - инициатор предложения
|
||||||
if (!hasAdminRights()) {
|
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||||
alert('❌ Для исполнения предложений необходимы права администратора');
|
if (!proposal) {
|
||||||
|
alert('❌ Предложение не найдено');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInitiator = address.value && proposal.initiator &&
|
||||||
|
address.value.toLowerCase() === proposal.initiator.toLowerCase();
|
||||||
|
|
||||||
|
if (!isInitiator) {
|
||||||
|
alert('❌ Только инициатор предложения может его исполнить');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeProposalAPI(dleAddress.value, proposalId);
|
console.log('[Debug] Попытка исполнения предложения:', proposalId);
|
||||||
|
console.log('[Debug] Адрес кошелька:', address.value);
|
||||||
|
|
||||||
await loadDleData();
|
// Получаем данные транзакции от backend
|
||||||
alert('✅ Предложение успешно исполнено!');
|
const result = await executeProposalAPI(dleAddress.value, proposalId);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[DleProposalsView] Данные транзакции исполнения получены:', result);
|
||||||
|
|
||||||
|
// Отправляем транзакцию через MetaMask
|
||||||
|
try {
|
||||||
|
// Проверяем валидность адреса
|
||||||
|
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||||
|
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем подключение к MetaMask
|
||||||
|
if (!window.ethereum) {
|
||||||
|
throw new Error('MetaMask не установлен');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрашиваем разрешение на подключение к MetaMask
|
||||||
|
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
|
||||||
|
if (accounts.length === 0) {
|
||||||
|
throw new Error('Нет подключенных аккаунтов в MetaMask');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
|
||||||
|
|
||||||
|
// Проверяем сеть
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
const expectedChainId = '0xaa36a7'; // Sepolia (11155111)
|
||||||
|
|
||||||
|
if (chainId !== expectedChainId) {
|
||||||
|
console.log(`[DleProposalsView] Неправильная сеть! Текущая: ${chainId}, ожидается: ${expectedChainId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: expectedChainId }],
|
||||||
|
});
|
||||||
|
} catch (switchError) {
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
console.log('[DleProposalsView] Добавляем Sepolia сеть');
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: expectedChainId,
|
||||||
|
chainName: 'Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'SepoliaETH',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Отправляем транзакцию исполнения:', {
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
});
|
||||||
|
|
||||||
|
const txHash = await window.ethereum.request({
|
||||||
|
method: 'eth_sendTransaction',
|
||||||
|
params: [{
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DleProposalsView] Транзакция исполнения отправлена:', txHash);
|
||||||
|
|
||||||
|
// Показываем уведомление с возможностью отслеживания
|
||||||
|
showTransactionNotification(txHash, 'Исполнение предложения отправлено!');
|
||||||
|
|
||||||
|
// Подписываемся на WebSocket уведомления о исполнении
|
||||||
|
subscribeToExecutionUpdates(txHash, proposalId);
|
||||||
|
|
||||||
|
// Запускаем отслеживание транзакции на backend
|
||||||
|
trackExecutionTransaction(txHash, dleAddress.value, proposalId);
|
||||||
|
|
||||||
|
} catch (txError) {
|
||||||
|
console.error('[DleProposalsView] Ошибка отправки транзакции исполнения:', txError);
|
||||||
|
alert('❌ Ошибка отправки транзакции исполнения: ' + txError.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при исполнении предложения:', error);
|
console.error('Ошибка при исполнении предложения:', error);
|
||||||
@@ -1764,6 +2501,14 @@ onUnmounted(() => {
|
|||||||
color: #721c24;
|
color: #721c24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.execution-notice {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #e2e3e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
.proposal-status.canceled {
|
.proposal-status.canceled {
|
||||||
background: #fff3cd;
|
background: #fff3cd;
|
||||||
color: #856404;
|
color: #856404;
|
||||||
@@ -1778,6 +2523,28 @@ onUnmounted(() => {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-details {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-details .detail-item {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.votes-container {
|
.votes-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -103,6 +103,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DLEReader -->
|
||||||
|
<div class="module-deploy-card">
|
||||||
|
<div class="module-content">
|
||||||
|
<h4>DLEReader</h4>
|
||||||
|
<p>Чтение данных DLE - API для получения информации о контракте и предложениях</p>
|
||||||
|
<div class="module-features">
|
||||||
|
<span class="feature-tag">API</span>
|
||||||
|
<span class="feature-tag">Чтение</span>
|
||||||
|
<span class="feature-tag">Данные</span>
|
||||||
|
<span class="feature-tag">Интеграция</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-deploy"
|
||||||
|
@click="router.push(`/management/modules/deploy/reader?address=${route.query.address}`)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-rocket"></i>
|
||||||
|
Деплой
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- CommunicationModule -->
|
<!-- CommunicationModule -->
|
||||||
<div class="module-deploy-card">
|
<div class="module-deploy-card">
|
||||||
<div class="module-content">
|
<div class="module-content">
|
||||||
@@ -465,12 +488,44 @@
|
|||||||
<div class="modules-list">
|
<div class="modules-list">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h3>📋 Модули DLE</h3>
|
<h3>📋 Модули DLE</h3>
|
||||||
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules">
|
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules || isLoadingDeploymentStatus">
|
||||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules }"></i> Обновить
|
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules || isLoadingDeploymentStatus }"></i> Обновить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLoadingModules" class="loading-modules">
|
<!-- Статус деплоя -->
|
||||||
|
<div v-if="isLoadingDeploymentStatus" class="deployment-status">
|
||||||
|
<div class="status-loading">
|
||||||
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
|
<span>Проверка статуса деплоя...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!canShowModules" class="deployment-status">
|
||||||
|
<div class="status-message" :class="deploymentStatus">
|
||||||
|
<div class="status-icon">
|
||||||
|
<i v-if="deploymentStatus === 'completed'" class="fas fa-check-circle"></i>
|
||||||
|
<i v-else-if="deploymentStatus === 'in_progress'" class="fas fa-spinner fa-spin"></i>
|
||||||
|
<i v-else-if="deploymentStatus === 'failed'" class="fas fa-exclamation-triangle"></i>
|
||||||
|
<i v-else-if="deploymentStatus === 'not_started'" class="fas fa-play-circle"></i>
|
||||||
|
<i v-else class="fas fa-question-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="status-content">
|
||||||
|
<h4>{{ deploymentStatusMessage }}</h4>
|
||||||
|
<p v-if="deploymentStatus === 'not_started'">
|
||||||
|
Для активации модулей необходимо запустить поэтапный деплой DLE.
|
||||||
|
</p>
|
||||||
|
<p v-else-if="deploymentStatus === 'failed'">
|
||||||
|
Проверьте логи деплоя и повторите попытку через форму деплоя.
|
||||||
|
</p>
|
||||||
|
<p v-else-if="deploymentStatus === 'in_progress'">
|
||||||
|
Дождитесь завершения деплоя. Модули станут доступны автоматически.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="isLoadingModules" class="loading-modules">
|
||||||
<p>Загрузка модулей...</p>
|
<p>Загрузка модулей...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -479,7 +534,7 @@
|
|||||||
<p>Используйте форму выше для добавления первого модуля</p>
|
<p>Используйте форму выше для добавления первого модуля</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="modules-grid">
|
<div v-else-if="canShowModules && modules.length > 0" class="modules-grid">
|
||||||
<div
|
<div
|
||||||
v-for="module in modules"
|
v-for="module in modules"
|
||||||
:key="module.moduleId"
|
:key="module.moduleId"
|
||||||
@@ -590,7 +645,9 @@ import {
|
|||||||
createRemoveModuleProposal,
|
createRemoveModuleProposal,
|
||||||
isModuleActive,
|
isModuleActive,
|
||||||
getModuleAddress,
|
getModuleAddress,
|
||||||
getAllModules
|
getAllModules,
|
||||||
|
getNetworksInfo,
|
||||||
|
getDeploymentStatus
|
||||||
} from '../../services/modulesService.js';
|
} from '../../services/modulesService.js';
|
||||||
import api from '../../api/axios';
|
import api from '../../api/axios';
|
||||||
|
|
||||||
@@ -612,11 +669,16 @@ const route = useRoute();
|
|||||||
const selectedDle = ref(null);
|
const selectedDle = ref(null);
|
||||||
const isLoadingDle = ref(false);
|
const isLoadingDle = ref(false);
|
||||||
const modules = ref([]);
|
const modules = ref([]);
|
||||||
|
const supportedNetworks = ref([]);
|
||||||
const isLoadingModules = ref(false);
|
const isLoadingModules = ref(false);
|
||||||
const isCreating = ref(false);
|
const isCreating = ref(false);
|
||||||
const isRemoving = ref(null);
|
const isRemoving = ref(null);
|
||||||
const isActivating = ref(null);
|
const isActivating = ref(null);
|
||||||
const isVerifying = ref(null);
|
const isVerifying = ref(null);
|
||||||
|
|
||||||
|
// Состояние деплоя
|
||||||
|
const deploymentStatus = ref('unknown'); // 'unknown', 'completed', 'in_progress', 'failed', 'not_started'
|
||||||
|
const isLoadingDeploymentStatus = ref(false);
|
||||||
const lastUpdateTime = ref('');
|
const lastUpdateTime = ref('');
|
||||||
|
|
||||||
// Форма нового модуля
|
// Форма нового модуля
|
||||||
@@ -641,6 +703,23 @@ const modulesCount = computed(() => modules.value.length);
|
|||||||
const activeModulesCount = computed(() => modules.value.filter(m => m.isActive).length);
|
const activeModulesCount = computed(() => modules.value.filter(m => m.isActive).length);
|
||||||
const inactiveModulesCount = computed(() => modules.value.filter(m => !m.isActive).length);
|
const inactiveModulesCount = computed(() => modules.value.filter(m => !m.isActive).length);
|
||||||
|
|
||||||
|
// Статус деплоя
|
||||||
|
const canShowModules = computed(() => deploymentStatus.value === 'completed');
|
||||||
|
const deploymentStatusMessage = computed(() => {
|
||||||
|
switch (deploymentStatus.value) {
|
||||||
|
case 'completed':
|
||||||
|
return 'Деплой завершен. Модули готовы к использованию.';
|
||||||
|
case 'in_progress':
|
||||||
|
return 'Деплой в процессе. Модули будут доступны после завершения.';
|
||||||
|
case 'failed':
|
||||||
|
return 'Деплой не удался. Проверьте логи и повторите попытку.';
|
||||||
|
case 'not_started':
|
||||||
|
return 'Деплой не начат. Запустите деплой для активации модулей.';
|
||||||
|
default:
|
||||||
|
return 'Статус деплоя неизвестен. Проверьте состояние системы.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Загрузка данных DLE
|
// Загрузка данных DLE
|
||||||
async function loadDleData() {
|
async function loadDleData() {
|
||||||
try {
|
try {
|
||||||
@@ -672,6 +751,37 @@ async function loadDleData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка статуса деплоя
|
||||||
|
async function checkDeploymentStatus() {
|
||||||
|
try {
|
||||||
|
isLoadingDeploymentStatus.value = true;
|
||||||
|
const dleAddress = route.query.address;
|
||||||
|
|
||||||
|
if (!dleAddress) {
|
||||||
|
console.warn('[ModulesView] Адрес DLE не найден для проверки статуса деплоя');
|
||||||
|
deploymentStatus.value = 'unknown';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ModulesView] Проверка статуса деплоя для DLE:', dleAddress);
|
||||||
|
|
||||||
|
const statusResponse = await getDeploymentStatus(dleAddress);
|
||||||
|
console.log('[ModulesView] Статус деплоя:', statusResponse);
|
||||||
|
|
||||||
|
if (statusResponse.success) {
|
||||||
|
deploymentStatus.value = statusResponse.data.status || 'unknown';
|
||||||
|
} else {
|
||||||
|
deploymentStatus.value = 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ModulesView] Ошибка при проверке статуса деплоя:', error);
|
||||||
|
deploymentStatus.value = 'unknown';
|
||||||
|
} finally {
|
||||||
|
isLoadingDeploymentStatus.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Загрузка модулей
|
// Загрузка модулей
|
||||||
async function loadModules() {
|
async function loadModules() {
|
||||||
try {
|
try {
|
||||||
@@ -681,15 +791,30 @@ async function loadModules() {
|
|||||||
if (!dleAddress) {
|
if (!dleAddress) {
|
||||||
console.error('[ModulesView] Адрес DLE не указан');
|
console.error('[ModulesView] Адрес DLE не указан');
|
||||||
modules.value = [];
|
modules.value = [];
|
||||||
|
supportedNetworks.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[ModulesView] Загрузка модулей для DLE:', dleAddress);
|
console.log('[ModulesView] Загрузка модулей для DLE:', dleAddress);
|
||||||
|
|
||||||
// Загружаем модули через modulesService
|
// Сначала проверяем статус деплоя
|
||||||
const modulesResponse = await getAllModules(dleAddress);
|
await checkDeploymentStatus();
|
||||||
|
|
||||||
console.log('[ModulesView] Ответ от API:', modulesResponse);
|
// Если деплой не завершен, не загружаем модули
|
||||||
|
if (deploymentStatus.value !== 'completed') {
|
||||||
|
console.log('[ModulesView] Деплой не завершен, модули не загружаются. Статус:', deploymentStatus.value);
|
||||||
|
modules.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем модули и информацию о сетях параллельно
|
||||||
|
const [modulesResponse, networksResponse] = await Promise.all([
|
||||||
|
getAllModules(dleAddress),
|
||||||
|
getNetworksInfo(dleAddress)
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('[ModulesView] Ответ от API модулей:', modulesResponse);
|
||||||
|
console.log('[ModulesView] Ответ от API сетей:', networksResponse);
|
||||||
|
|
||||||
if (modulesResponse.success) {
|
if (modulesResponse.success) {
|
||||||
modules.value = modulesResponse.data.modules || [];
|
modules.value = modulesResponse.data.modules || [];
|
||||||
@@ -697,7 +822,7 @@ async function loadModules() {
|
|||||||
count: modules.value.length,
|
count: modules.value.length,
|
||||||
modules: modules.value.map(m => ({
|
modules: modules.value.map(m => ({
|
||||||
name: m.moduleName,
|
name: m.moduleName,
|
||||||
address: m.moduleAddress,
|
addresses: m.addresses?.length || 0,
|
||||||
active: m.isActive,
|
active: m.isActive,
|
||||||
id: m.moduleId
|
id: m.moduleId
|
||||||
})),
|
})),
|
||||||
@@ -717,6 +842,20 @@ async function loadModules() {
|
|||||||
console.error('[ModulesView] Ошибка загрузки модулей:', modulesResponse.error);
|
console.error('[ModulesView] Ошибка загрузки модулей:', modulesResponse.error);
|
||||||
modules.value = [];
|
modules.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (networksResponse.success) {
|
||||||
|
supportedNetworks.value = networksResponse.data.networks || [];
|
||||||
|
console.log('[ModulesView] Сети загружены успешно:', {
|
||||||
|
count: supportedNetworks.value.length,
|
||||||
|
networks: supportedNetworks.value.map(n => ({
|
||||||
|
name: n.networkName,
|
||||||
|
chainId: n.chainId
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('[ModulesView] Ошибка загрузки сетей:', networksResponse.error);
|
||||||
|
supportedNetworks.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ModulesView] Ошибка загрузки модулей:', error);
|
console.error('[ModulesView] Ошибка загрузки модулей:', error);
|
||||||
@@ -726,6 +865,7 @@ async function loadModules() {
|
|||||||
status: error.response?.status
|
status: error.response?.status
|
||||||
});
|
});
|
||||||
modules.value = [];
|
modules.value = [];
|
||||||
|
supportedNetworks.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingModules.value = false;
|
isLoadingModules.value = false;
|
||||||
}
|
}
|
||||||
@@ -754,22 +894,112 @@ async function handleCreateAddModuleProposal() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('[ModulesView] Предложение создано:', result);
|
console.log('[ModulesView] Данные транзакции получены:', result);
|
||||||
alert('✅ Предложение для добавления модуля создано!');
|
|
||||||
|
|
||||||
// Очищаем форму
|
// Отправляем транзакцию через MetaMask
|
||||||
newModule.value = {
|
try {
|
||||||
moduleId: '',
|
// Проверяем валидность адреса
|
||||||
moduleAddress: '',
|
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||||
description: '',
|
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||||
duration: 86400,
|
}
|
||||||
chainId: 11155111
|
|
||||||
};
|
// Проверяем, что адрес в правильном формате (checksum)
|
||||||
|
const isValidAddress = /^0x[a-fA-F0-9]{40}$/.test(result.data.to);
|
||||||
// Перезагружаем модули
|
if (!isValidAddress) {
|
||||||
await loadModules();
|
throw new Error(`Адрес не в правильном формате: ${result.data.to}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что есть подключенный аккаунт
|
||||||
|
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
console.log('[ModulesView] Запрашиваем разрешение на подключение к MetaMask');
|
||||||
|
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ModulesView] Подключенный аккаунт:', accounts[0]);
|
||||||
|
|
||||||
|
// Проверяем подключение к правильной сети
|
||||||
|
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
const expectedChainId = '0x' + newModule.value.chainId.toString(16);
|
||||||
|
|
||||||
|
if (chainId !== expectedChainId) {
|
||||||
|
console.log(`[ModulesView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Пытаемся переключиться на Sepolia
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: expectedChainId }],
|
||||||
|
});
|
||||||
|
console.log('[ModulesView] Успешно переключились на Sepolia');
|
||||||
|
} catch (switchError) {
|
||||||
|
// Если сеть не добавлена, добавляем её
|
||||||
|
if (switchError.code === 4902) {
|
||||||
|
console.log('[ModulesView] Добавляем Sepolia сеть');
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: expectedChainId,
|
||||||
|
chainName: 'Sepolia',
|
||||||
|
nativeCurrency: {
|
||||||
|
name: 'SepoliaETH',
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18
|
||||||
|
},
|
||||||
|
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||||
|
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ModulesView] Отправляем транзакцию:', {
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
});
|
||||||
|
|
||||||
|
const txHash = await window.ethereum.request({
|
||||||
|
method: 'eth_sendTransaction',
|
||||||
|
params: [{
|
||||||
|
from: accounts[0],
|
||||||
|
to: result.data.to,
|
||||||
|
data: result.data.data,
|
||||||
|
value: result.data.value,
|
||||||
|
gas: result.data.gasLimit
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[ModulesView] Транзакция отправлена:', txHash);
|
||||||
|
alert(`✅ Транзакция отправлена! Hash: ${txHash}`);
|
||||||
|
|
||||||
|
// Очищаем форму
|
||||||
|
newModule.value = {
|
||||||
|
moduleId: '',
|
||||||
|
moduleAddress: '',
|
||||||
|
description: '',
|
||||||
|
duration: 86400,
|
||||||
|
chainId: 11155111
|
||||||
|
};
|
||||||
|
|
||||||
|
// Перезагружаем модули
|
||||||
|
await loadModules();
|
||||||
|
|
||||||
|
} catch (txError) {
|
||||||
|
console.error('[ModulesView] Ошибка отправки транзакции:', txError);
|
||||||
|
alert('❌ Ошибка отправки транзакции: ' + txError.message);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('❌ Ошибка создания предложения: ' + result.error);
|
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -854,7 +1084,8 @@ async function verifyModule(module, addressInfo) {
|
|||||||
dleAddress: dleAddress,
|
dleAddress: dleAddress,
|
||||||
moduleId: module.moduleId,
|
moduleId: module.moduleId,
|
||||||
moduleAddress: addressInfo.address,
|
moduleAddress: addressInfo.address,
|
||||||
moduleName: module.moduleName
|
moduleName: module.moduleName,
|
||||||
|
chainId: addressInfo.chainId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
@@ -898,16 +1129,12 @@ function getVerificationButtonTitle(verificationStatus) {
|
|||||||
|
|
||||||
// Утилиты
|
// Утилиты
|
||||||
function getEtherscanUrl(address, networkIndex, chainId) {
|
function getEtherscanUrl(address, networkIndex, chainId) {
|
||||||
// Если есть chainId, используем его для определения правильного URL
|
// Если есть chainId, ищем информацию о сети в supportedNetworks
|
||||||
if (chainId) {
|
if (chainId && supportedNetworks.value.length > 0) {
|
||||||
const networkUrls = {
|
const network = supportedNetworks.value.find(n => n.chainId === chainId);
|
||||||
11155111: `https://sepolia.etherscan.io/address/${address}`, // Sepolia
|
if (network && network.etherscanUrl) {
|
||||||
17000: `https://holesky.etherscan.io/address/${address}`, // Holesky
|
return `${network.etherscanUrl}/address/${address}`;
|
||||||
421614: `https://sepolia.arbiscan.io/address/${address}`, // Arbitrum Sepolia
|
}
|
||||||
84532: `https://sepolia.basescan.org/address/${address}` // Base Sepolia
|
|
||||||
};
|
|
||||||
|
|
||||||
return networkUrls[chainId] || `https://etherscan.io/address/${address}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback на старую логику по networkIndex (для обратной совместимости)
|
// Fallback на старую логику по networkIndex (для обратной совместимости)
|
||||||
@@ -1204,6 +1431,102 @@ onMounted(() => {
|
|||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Статус деплоя */
|
||||||
|
.deployment-status {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-loading i {
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-loading span {
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.completed {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.in_progress {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.failed {
|
||||||
|
background-color: #ffebee;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.not_started {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.unknown {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.completed .status-icon {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.in_progress .status-icon {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.failed .status-icon {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.not_started .status-icon {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message.unknown .status-icon {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content h4 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-content p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.list-header {
|
.list-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is proprietary and confidential.
|
||||||
|
Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
|
||||||
|
For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
Website: https://hb3-accelerator.com
|
||||||
|
GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout
|
||||||
|
:is-authenticated="isAuthenticated"
|
||||||
|
:identities="identities"
|
||||||
|
:token-balances="tokenBalances"
|
||||||
|
:is-loading-tokens="isLoadingTokens"
|
||||||
|
@auth-action-completed="$emit('auth-action-completed')"
|
||||||
|
>
|
||||||
|
<div class="reader-module-deploy">
|
||||||
|
<!-- Заголовок -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<h1>Деплой DLEReader</h1>
|
||||||
|
<p>API для чтения данных DLE - получение информации о контракте и предложениях</p>
|
||||||
|
<p v-if="dleAddress" class="dle-address">
|
||||||
|
<strong>DLE:</strong> {{ dleAddress }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button class="close-btn" @click="router.push('/management/modules')">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Информация о модуле -->
|
||||||
|
<div class="module-info">
|
||||||
|
<div class="info-card">
|
||||||
|
<h3>📊 DLEReader</h3>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>Назначение:</strong> Чтение данных DLE контракта
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>Функции:</strong> API для предложений, голосования, статистики
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<strong>Безопасность:</strong> Только чтение, не изменяет состояние
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Форма деплоя модуля во всех сетях -->
|
||||||
|
<div class="deploy-form">
|
||||||
|
<div class="form-header">
|
||||||
|
<h3>🌐 Деплой DLEReader во всех сетях</h3>
|
||||||
|
<p>Деплой API модуля для чтения данных во всех 4 сетях одновременно</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-content">
|
||||||
|
<!-- Информация о сетях -->
|
||||||
|
<div class="networks-info">
|
||||||
|
<h4>📡 Сети для деплоя:</h4>
|
||||||
|
<div class="networks-list">
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 11155111</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Holesky</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 17000</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Arbitrum Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 421614</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Base Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 84532</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Настройки модуля -->
|
||||||
|
<div class="module-settings">
|
||||||
|
<h4>⚙️ Настройки DLEReader:</h4>
|
||||||
|
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chainId">ID сети:</label>
|
||||||
|
<select
|
||||||
|
id="chainId"
|
||||||
|
v-model="moduleSettings.chainId"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="11155111">Sepolia (11155111)</option>
|
||||||
|
<option value="17000">Holesky (17000)</option>
|
||||||
|
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||||
|
<option value="84532">Base Sepolia (84532)</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">ID сети для деплоя модуля</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="simple-info">
|
||||||
|
<h5>📋 Информация о DLEReader:</h5>
|
||||||
|
<div class="info-text">
|
||||||
|
<p><strong>DLEReader</strong> - это простой read-only модуль, который:</p>
|
||||||
|
<ul>
|
||||||
|
<li>✅ Только читает данные из DLE контракта</li>
|
||||||
|
<li>✅ Не изменяет состояние блокчейна</li>
|
||||||
|
<li>✅ Предоставляет API для получения информации</li>
|
||||||
|
<li>✅ Безопасен для обновления</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Конструктор принимает только один параметр:</strong> адрес DLE контракта</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка деплоя -->
|
||||||
|
<div class="deploy-actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-large deploy-module"
|
||||||
|
@click="deployDLEReader"
|
||||||
|
:disabled="isDeploying || !dleAddress"
|
||||||
|
>
|
||||||
|
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||||
|
{{ isDeploying ? 'Деплой модуля...' : 'Деплой DLEReader' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="deploymentProgress" class="deployment-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>{{ deploymentProgress.message }}</span>
|
||||||
|
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import BaseLayout from '../../../components/BaseLayout.vue';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
isAuthenticated: Boolean,
|
||||||
|
identities: Array,
|
||||||
|
tokenBalances: Object,
|
||||||
|
isLoadingTokens: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['auth-action-completed']);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// Состояние
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const dleAddress = ref(route.query.address || null);
|
||||||
|
const isDeploying = ref(false);
|
||||||
|
const deploymentProgress = ref(null);
|
||||||
|
|
||||||
|
// Настройки модуля
|
||||||
|
const moduleSettings = ref({
|
||||||
|
// Единственный параметр - ID сети
|
||||||
|
chainId: 11155111
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция деплоя DLEReader
|
||||||
|
async function deployDLEReader() {
|
||||||
|
try {
|
||||||
|
isDeploying.value = true;
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Инициализация деплоя...',
|
||||||
|
percentage: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[DLEReaderDeployView] Начинаем деплой DLEReader для DLE:', dleAddress.value);
|
||||||
|
|
||||||
|
// Вызываем API для деплоя модуля во всех сетях
|
||||||
|
const response = await fetch('/api/dle-modules/deploy-reader', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dleAddress: dleAddress.value,
|
||||||
|
moduleType: 'reader',
|
||||||
|
settings: {
|
||||||
|
// Единственный параметр - ID сети
|
||||||
|
chainId: moduleSettings.value.chainId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[DLEReaderDeployView] Деплой успешно запущен:', result);
|
||||||
|
|
||||||
|
// Обновляем прогресс
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
|
||||||
|
percentage: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
alert('✅ Деплой DLEReader запущен во всех сетях!');
|
||||||
|
|
||||||
|
// Перенаправляем обратно к модулям
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Неизвестная ошибка');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLEReaderDeployView] Ошибка деплоя:', error);
|
||||||
|
alert('❌ Ошибка деплоя: ' + error.message);
|
||||||
|
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Ошибка деплоя: ' + error.message,
|
||||||
|
percentage: 0
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
isDeploying.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('[DLEReaderDeployView] Страница загружена');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reader-module-deploy {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h1 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content p {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dle-address {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Информация о модуле */
|
||||||
|
.module-info {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border-left: 4px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item strong {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Форма деплоя */
|
||||||
|
.deploy-form {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header p {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-info,
|
||||||
|
.module-settings {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Настройки отображения данных */
|
||||||
|
.data-display-settings {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-display-settings h5 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-display-settings .form-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-display-settings .form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-display-settings .form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Простая информация */
|
||||||
|
.simple-info {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-info h5 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text p {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text ul {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text li {
|
||||||
|
margin: 5px 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text strong {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-actions {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 16px 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-progress {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Сети */
|
||||||
|
.networks-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-chain-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -49,11 +49,255 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Форма деплоя будет добавлена позже -->
|
<!-- Форма деплоя модуля во всех сетях -->
|
||||||
<div class="deploy-form-placeholder">
|
<div class="deploy-form">
|
||||||
<div class="placeholder-content">
|
<div class="form-header">
|
||||||
<h3>🚧 Форма деплоя в разработке</h3>
|
<h3>🌐 Деплой TimelockModule во всех сетях</h3>
|
||||||
<p>Здесь будет форма для деплоя TimelockModule</p>
|
<p>Деплой модуля временных задержек во всех 4 сетях одновременно</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-content">
|
||||||
|
<!-- Информация о сетях -->
|
||||||
|
<div class="networks-info">
|
||||||
|
<h4>📡 Сети для деплоя:</h4>
|
||||||
|
<div class="networks-list">
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 11155111</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Holesky</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 17000</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Arbitrum Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 421614</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Base Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 84532</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Настройки модуля -->
|
||||||
|
<div class="module-settings">
|
||||||
|
<h4>⚙️ Настройки TimelockModule:</h4>
|
||||||
|
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chainId">ID сети:</label>
|
||||||
|
<select
|
||||||
|
id="chainId"
|
||||||
|
v-model="moduleSettings.chainId"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="11155111">Sepolia (11155111)</option>
|
||||||
|
<option value="17000">Holesky (17000)</option>
|
||||||
|
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||||
|
<option value="84532">Base Sepolia (84532)</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">ID сети для деплоя модуля</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="defaultDelay">Стандартная задержка (дни):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="defaultDelay"
|
||||||
|
v-model="moduleSettings.defaultDelay"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="30"
|
||||||
|
placeholder="2"
|
||||||
|
>
|
||||||
|
<small class="form-help">Стандартная задержка для операций (1-30 дней)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="emergencyDelay"
|
||||||
|
v-model="moduleSettings.emergencyDelay"
|
||||||
|
class="form-control"
|
||||||
|
min="5"
|
||||||
|
max="1440"
|
||||||
|
placeholder="30"
|
||||||
|
>
|
||||||
|
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="maxDelay">Максимальная задержка (дни):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="maxDelay"
|
||||||
|
v-model="moduleSettings.maxDelay"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
placeholder="30"
|
||||||
|
>
|
||||||
|
<small class="form-help">Максимальная задержка для операций (1-365 дней)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="minDelay">Минимальная задержка (часы):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="minDelay"
|
||||||
|
v-model="moduleSettings.minDelay"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="720"
|
||||||
|
placeholder="24"
|
||||||
|
>
|
||||||
|
<small class="form-help">Минимальная задержка для операций (1-720 часов)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="maxOperations">Максимум операций в очереди:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="maxOperations"
|
||||||
|
v-model="moduleSettings.maxOperations"
|
||||||
|
class="form-control"
|
||||||
|
min="10"
|
||||||
|
max="1000"
|
||||||
|
placeholder="100"
|
||||||
|
>
|
||||||
|
<small class="form-help">Максимальное количество операций в очереди (10-1000)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Дополнительные настройки таймлока -->
|
||||||
|
<div class="advanced-settings">
|
||||||
|
<h5>🔧 Дополнительные настройки таймлока:</h5>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="criticalOperations">Критические операции (JSON формат):</label>
|
||||||
|
<textarea
|
||||||
|
id="criticalOperations"
|
||||||
|
v-model="moduleSettings.criticalOperations"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder='["0x12345678", "0x87654321"]'
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Селекторы функций, которые считаются критическими (JSON массив)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emergencyOperations">Экстренные операции (JSON формат):</label>
|
||||||
|
<textarea
|
||||||
|
id="emergencyOperations"
|
||||||
|
v-model="moduleSettings.emergencyOperations"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder='["0xabcdef12", "0x21fedcba"]'
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Селекторы функций для экстренных операций (JSON массив)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="operationDelays">Задержки для операций (JSON формат):</label>
|
||||||
|
<textarea
|
||||||
|
id="operationDelays"
|
||||||
|
v-model="moduleSettings.operationDelays"
|
||||||
|
class="form-control"
|
||||||
|
rows="4"
|
||||||
|
placeholder='{"0x12345678": 86400, "0x87654321": 172800}'
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Кастомные задержки для конкретных операций (селектор => секунды)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="autoExecuteEnabled">Автоисполнение включено:</label>
|
||||||
|
<select
|
||||||
|
id="autoExecuteEnabled"
|
||||||
|
v-model="moduleSettings.autoExecuteEnabled"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option value="true">Включено</option>
|
||||||
|
<option value="false">Отключено</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">Автоматическое исполнение операций после истечения задержки</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cancellationWindow">Окно отмены (часы):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="cancellationWindow"
|
||||||
|
v-model="moduleSettings.cancellationWindow"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="168"
|
||||||
|
placeholder="24"
|
||||||
|
>
|
||||||
|
<small class="form-help">Время, в течение которого можно отменить операцию (1-168 часов)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="executionWindow">Окно исполнения (часы):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="executionWindow"
|
||||||
|
v-model="moduleSettings.executionWindow"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="168"
|
||||||
|
placeholder="48"
|
||||||
|
>
|
||||||
|
<small class="form-help">Время, в течение которого можно исполнить операцию (1-168 часов)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="timelockDescription">Описание таймлока:</label>
|
||||||
|
<textarea
|
||||||
|
id="timelockDescription"
|
||||||
|
v-model="moduleSettings.timelockDescription"
|
||||||
|
class="form-control"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Описание таймлока DLE для безопасности операций..."
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Описание таймлока для документации</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка деплоя -->
|
||||||
|
<div class="deploy-actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-large deploy-module"
|
||||||
|
@click="deployTimelockModule"
|
||||||
|
:disabled="isDeploying || !dleAddress"
|
||||||
|
>
|
||||||
|
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||||
|
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TimelockModule' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="deploymentProgress" class="deployment-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>{{ deploymentProgress.message }}</span>
|
||||||
|
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,6 +327,108 @@ const route = useRoute();
|
|||||||
// Состояние
|
// Состояние
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const dleAddress = ref(route.query.address || null);
|
const dleAddress = ref(route.query.address || null);
|
||||||
|
const isDeploying = ref(false);
|
||||||
|
const deploymentProgress = ref(null);
|
||||||
|
|
||||||
|
// Настройки модуля
|
||||||
|
const moduleSettings = ref({
|
||||||
|
// Основные параметры
|
||||||
|
chainId: 11155111,
|
||||||
|
defaultDelay: 2, // days
|
||||||
|
emergencyDelay: 30, // minutes
|
||||||
|
maxDelay: 30, // days
|
||||||
|
minDelay: 24, // hours
|
||||||
|
|
||||||
|
// Дополнительные настройки
|
||||||
|
maxOperations: 100,
|
||||||
|
criticalOperations: '',
|
||||||
|
emergencyOperations: '',
|
||||||
|
operationDelays: '',
|
||||||
|
autoExecuteEnabled: 'true',
|
||||||
|
cancellationWindow: 24, // hours
|
||||||
|
executionWindow: 48, // hours
|
||||||
|
timelockDescription: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция деплоя TimelockModule
|
||||||
|
async function deployTimelockModule() {
|
||||||
|
try {
|
||||||
|
isDeploying.value = true;
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Инициализация деплоя...',
|
||||||
|
percentage: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[TimelockModuleDeployView] Начинаем деплой TimelockModule для DLE:', dleAddress.value);
|
||||||
|
|
||||||
|
// Вызываем API для деплоя модуля во всех сетях
|
||||||
|
const response = await fetch('/api/dle-modules/deploy-timelock', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dleAddress: dleAddress.value,
|
||||||
|
moduleType: 'timelock',
|
||||||
|
settings: {
|
||||||
|
// Основные параметры
|
||||||
|
chainId: moduleSettings.value.chainId,
|
||||||
|
defaultDelay: moduleSettings.value.defaultDelay * 24 * 60 * 60, // конвертируем дни в секунды
|
||||||
|
emergencyDelay: moduleSettings.value.emergencyDelay * 60, // конвертируем минуты в секунды
|
||||||
|
maxDelay: moduleSettings.value.maxDelay * 24 * 60 * 60, // конвертируем дни в секунды
|
||||||
|
minDelay: moduleSettings.value.minDelay * 60 * 60, // конвертируем часы в секунды
|
||||||
|
|
||||||
|
// Дополнительные настройки
|
||||||
|
maxOperations: parseInt(moduleSettings.value.maxOperations),
|
||||||
|
criticalOperations: moduleSettings.value.criticalOperations ? JSON.parse(moduleSettings.value.criticalOperations) : [],
|
||||||
|
emergencyOperations: moduleSettings.value.emergencyOperations ? JSON.parse(moduleSettings.value.emergencyOperations) : [],
|
||||||
|
operationDelays: moduleSettings.value.operationDelays ? JSON.parse(moduleSettings.value.operationDelays) : {},
|
||||||
|
autoExecuteEnabled: moduleSettings.value.autoExecuteEnabled === 'true',
|
||||||
|
cancellationWindow: moduleSettings.value.cancellationWindow * 60 * 60, // конвертируем часы в секунды
|
||||||
|
executionWindow: moduleSettings.value.executionWindow * 60 * 60, // конвертируем часы в секунды
|
||||||
|
timelockDescription: moduleSettings.value.timelockDescription
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[TimelockModuleDeployView] Деплой успешно запущен:', result);
|
||||||
|
|
||||||
|
// Обновляем прогресс
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
|
||||||
|
percentage: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
alert('✅ Деплой TimelockModule запущен во всех сетях!');
|
||||||
|
|
||||||
|
// Перенаправляем обратно к модулям
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Неизвестная ошибка');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TimelockModuleDeployView] Ошибка деплоя:', error);
|
||||||
|
alert('❌ Ошибка деплоя: ' + error.message);
|
||||||
|
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Ошибка деплоя: ' + error.message,
|
||||||
|
percentage: 0
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
isDeploying.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -150,6 +496,182 @@ onMounted(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Форма деплоя */
|
||||||
|
.deploy-form {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header p {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-info,
|
||||||
|
.module-settings {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Дополнительные настройки */
|
||||||
|
.advanced-settings {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings h5 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings .form-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings .form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings .form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-actions {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 16px 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-progress {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* Информация о модуле */
|
/* Информация о модуле */
|
||||||
.module-info {
|
.module-info {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
|||||||
@@ -49,11 +49,263 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Форма деплоя будет добавлена позже -->
|
<!-- Форма деплоя модуля во всех сетях -->
|
||||||
<div class="deploy-form-placeholder">
|
<div class="deploy-form">
|
||||||
<div class="placeholder-content">
|
<div class="form-header">
|
||||||
<h3>🚧 Форма деплоя в разработке</h3>
|
<h3>🌐 Деплой TreasuryModule во всех сетях</h3>
|
||||||
<p>Здесь будет форма для деплоя TreasuryModule</p>
|
<p>Деплой модуля казначейства во всех 4 сетях одновременно</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-content">
|
||||||
|
<!-- Информация о сетях -->
|
||||||
|
<div class="networks-info">
|
||||||
|
<h4>📡 Сети для деплоя:</h4>
|
||||||
|
<div class="networks-list">
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 11155111</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Holesky</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 17000</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Arbitrum Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 421614</span>
|
||||||
|
</div>
|
||||||
|
<div class="network-item">
|
||||||
|
<span class="network-name">Base Sepolia</span>
|
||||||
|
<span class="network-chain-id">Chain ID: 84532</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Настройки модуля -->
|
||||||
|
<div class="module-settings">
|
||||||
|
<h4>⚙️ Настройки TreasuryModule:</h4>
|
||||||
|
|
||||||
|
<div class="settings-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emergencyAdmin">Адрес экстренного администратора:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="emergencyAdmin"
|
||||||
|
v-model="moduleSettings.emergencyAdmin"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="0x..."
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<small class="form-help">Адрес экстренного администратора для управления модулем</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chainId">ID сети:</label>
|
||||||
|
<select
|
||||||
|
id="chainId"
|
||||||
|
v-model="moduleSettings.chainId"
|
||||||
|
class="form-control"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="11155111">Sepolia (11155111)</option>
|
||||||
|
<option value="17000">Holesky (17000)</option>
|
||||||
|
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||||
|
<option value="84532">Base Sepolia (84532)</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">ID сети для деплоя модуля</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="defaultDelay">Стандартная задержка (часы):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="defaultDelay"
|
||||||
|
v-model="moduleSettings.defaultDelay"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="720"
|
||||||
|
placeholder="24"
|
||||||
|
>
|
||||||
|
<small class="form-help">Стандартная задержка для операций (1-720 часов)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="emergencyDelay"
|
||||||
|
v-model="moduleSettings.emergencyDelay"
|
||||||
|
class="form-control"
|
||||||
|
min="5"
|
||||||
|
max="1440"
|
||||||
|
placeholder="30"
|
||||||
|
>
|
||||||
|
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="supportedTokens">Поддерживаемые токены (адреса через запятую):</label>
|
||||||
|
<textarea
|
||||||
|
id="supportedTokens"
|
||||||
|
v-model="moduleSettings.supportedTokens"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder="0x1234..., 0x5678..., 0x9abc..."
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Адреса ERC20 токенов, которые будет поддерживать казначейство (через запятую)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="gasPaymentTokens">Токены для оплаты газа (адреса через запятую):</label>
|
||||||
|
<textarea
|
||||||
|
id="gasPaymentTokens"
|
||||||
|
v-model="moduleSettings.gasPaymentTokens"
|
||||||
|
class="form-control"
|
||||||
|
rows="2"
|
||||||
|
placeholder="0x1234..., 0x5678..."
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Токены, которыми можно оплачивать газ (через запятую)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Дополнительные настройки казны -->
|
||||||
|
<div class="advanced-settings">
|
||||||
|
<h5>🔧 Дополнительные настройки казны:</h5>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="paymasterAddress">Адрес Paymaster:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="paymasterAddress"
|
||||||
|
v-model="moduleSettings.paymasterAddress"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="0x..."
|
||||||
|
>
|
||||||
|
<small class="form-help">Адрес Paymaster для ERC-4337 (оплата газа любым токеном)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="maxBatchTransfers">Максимум batch переводов:</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="maxBatchTransfers"
|
||||||
|
v-model="moduleSettings.maxBatchTransfers"
|
||||||
|
class="form-control"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
placeholder="50"
|
||||||
|
>
|
||||||
|
<small class="form-help">Максимальное количество переводов в batch операции (1-100)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="gasTokenRates">Курсы токенов для газа (JSON формат):</label>
|
||||||
|
<textarea
|
||||||
|
id="gasTokenRates"
|
||||||
|
v-model="moduleSettings.gasTokenRates"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
placeholder='{"0x1234...": "1000000000000000000", "0x5678...": "2000000000000000000"}'
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Курсы обмена токенов на нативную монету (JSON формат)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="emergencyThreshold">Порог экстренных операций (ETH):</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="emergencyThreshold"
|
||||||
|
v-model="moduleSettings.emergencyThreshold"
|
||||||
|
class="form-control"
|
||||||
|
min="0"
|
||||||
|
step="0.001"
|
||||||
|
placeholder="1.0"
|
||||||
|
>
|
||||||
|
<small class="form-help">Порог для экстренных операций в ETH</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="initialTokens">Начальные токены для добавления (JSON формат):</label>
|
||||||
|
<textarea
|
||||||
|
id="initialTokens"
|
||||||
|
v-model="moduleSettings.initialTokens"
|
||||||
|
class="form-control"
|
||||||
|
rows="4"
|
||||||
|
placeholder='[{"address": "0x1234...", "symbol": "USDC", "decimals": 6}, {"address": "0x5678...", "symbol": "USDT", "decimals": 6}]'
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Токены для автоматического добавления при деплое (JSON массив)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="autoRefreshBalances">Автообновление балансов:</label>
|
||||||
|
<select
|
||||||
|
id="autoRefreshBalances"
|
||||||
|
v-model="moduleSettings.autoRefreshBalances"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option value="true">Включено</option>
|
||||||
|
<option value="false">Отключено</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">Автоматическое обновление балансов токенов</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="batchTransferEnabled">Batch переводы включены:</label>
|
||||||
|
<select
|
||||||
|
id="batchTransferEnabled"
|
||||||
|
v-model="moduleSettings.batchTransferEnabled"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option value="true">Включено</option>
|
||||||
|
<option value="false">Отключено</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-help">Разрешить batch операции переводов</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="treasuryDescription">Описание казны:</label>
|
||||||
|
<textarea
|
||||||
|
id="treasuryDescription"
|
||||||
|
v-model="moduleSettings.treasuryDescription"
|
||||||
|
class="form-control"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Описание казны DLE для управления финансами..."
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">Описание казны для документации</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка деплоя -->
|
||||||
|
<div class="deploy-actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-large deploy-module"
|
||||||
|
@click="deployTreasuryModule"
|
||||||
|
:disabled="isDeploying || !dleAddress"
|
||||||
|
>
|
||||||
|
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||||
|
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TreasuryModule' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="deploymentProgress" class="deployment-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>{{ deploymentProgress.message }}</span>
|
||||||
|
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,6 +335,114 @@ const route = useRoute();
|
|||||||
// Состояние
|
// Состояние
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const dleAddress = ref(route.query.address || null);
|
const dleAddress = ref(route.query.address || null);
|
||||||
|
const isDeploying = ref(false);
|
||||||
|
const deploymentProgress = ref(null);
|
||||||
|
|
||||||
|
// Настройки модуля
|
||||||
|
const moduleSettings = ref({
|
||||||
|
// Основные параметры
|
||||||
|
emergencyAdmin: '',
|
||||||
|
chainId: 11155111,
|
||||||
|
defaultDelay: 24, // hours
|
||||||
|
emergencyDelay: 30, // minutes
|
||||||
|
|
||||||
|
// Токены
|
||||||
|
supportedTokens: '',
|
||||||
|
gasPaymentTokens: '',
|
||||||
|
initialTokens: '',
|
||||||
|
|
||||||
|
// Дополнительные настройки
|
||||||
|
paymasterAddress: '',
|
||||||
|
maxBatchTransfers: 50,
|
||||||
|
gasTokenRates: '',
|
||||||
|
emergencyThreshold: 1.0,
|
||||||
|
autoRefreshBalances: 'true',
|
||||||
|
batchTransferEnabled: 'true',
|
||||||
|
treasuryDescription: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция деплоя TreasuryModule
|
||||||
|
async function deployTreasuryModule() {
|
||||||
|
try {
|
||||||
|
isDeploying.value = true;
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Инициализация деплоя...',
|
||||||
|
percentage: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[TreasuryModuleDeployView] Начинаем деплой TreasuryModule для DLE:', dleAddress.value);
|
||||||
|
|
||||||
|
// Вызываем API для деплоя модуля во всех сетях
|
||||||
|
const response = await fetch('/api/dle-modules/deploy-treasury', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dleAddress: dleAddress.value,
|
||||||
|
moduleType: 'treasury',
|
||||||
|
settings: {
|
||||||
|
// Основные параметры
|
||||||
|
emergencyAdmin: moduleSettings.value.emergencyAdmin,
|
||||||
|
chainId: moduleSettings.value.chainId,
|
||||||
|
defaultDelay: moduleSettings.value.defaultDelay,
|
||||||
|
emergencyDelay: moduleSettings.value.emergencyDelay,
|
||||||
|
|
||||||
|
// Токены
|
||||||
|
supportedTokens: moduleSettings.value.supportedTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
|
||||||
|
gasPaymentTokens: moduleSettings.value.gasPaymentTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
|
||||||
|
initialTokens: moduleSettings.value.initialTokens ? JSON.parse(moduleSettings.value.initialTokens) : [],
|
||||||
|
|
||||||
|
// Дополнительные настройки
|
||||||
|
paymasterAddress: moduleSettings.value.paymasterAddress,
|
||||||
|
maxBatchTransfers: parseInt(moduleSettings.value.maxBatchTransfers),
|
||||||
|
gasTokenRates: moduleSettings.value.gasTokenRates ? JSON.parse(moduleSettings.value.gasTokenRates) : {},
|
||||||
|
emergencyThreshold: parseFloat(moduleSettings.value.emergencyThreshold),
|
||||||
|
autoRefreshBalances: moduleSettings.value.autoRefreshBalances === 'true',
|
||||||
|
batchTransferEnabled: moduleSettings.value.batchTransferEnabled === 'true',
|
||||||
|
treasuryDescription: moduleSettings.value.treasuryDescription
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('[TreasuryModuleDeployView] Деплой успешно запущен:', result);
|
||||||
|
|
||||||
|
// Обновляем прогресс
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
|
||||||
|
percentage: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
alert('✅ Деплой TreasuryModule запущен во всех сетях!');
|
||||||
|
|
||||||
|
// Перенаправляем обратно к модулям
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Неизвестная ошибка');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TreasuryModuleDeployView] Ошибка деплоя:', error);
|
||||||
|
alert('❌ Ошибка деплоя: ' + error.message);
|
||||||
|
|
||||||
|
deploymentProgress.value = {
|
||||||
|
message: 'Ошибка деплоя: ' + error.message,
|
||||||
|
percentage: 0
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
isDeploying.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -150,6 +510,240 @@ onMounted(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Форма деплоя */
|
||||||
|
.deploy-form {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header p {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-info,
|
||||||
|
.module-settings {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Дополнительные настройки */
|
||||||
|
.advanced-settings {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings h5 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings .form-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings .form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings .form-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-info h4,
|
||||||
|
.deploy-parameters h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.networks-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-chain-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-value {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deploy-actions {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 16px 32px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment-progress {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* Информация о модуле */
|
/* Информация о модуле */
|
||||||
.module-info {
|
.module-info {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
|
|||||||
27
frontend/src/views/smartcontracts/modules/index.js
Normal file
27
frontend/src/views/smartcontracts/modules/index.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Основные модули DLE
|
||||||
|
export { default as TreasuryModuleDeployView } from './TreasuryModuleDeployView.vue';
|
||||||
|
export { default as TimelockModuleDeployView } from './TimelockModuleDeployView.vue';
|
||||||
|
export { default as DLEReaderDeployView } from './DLEReaderDeployView.vue';
|
||||||
|
|
||||||
|
// Дополнительные модули
|
||||||
|
export { default as CommunicationModuleDeployView } from './CommunicationModuleDeployView.vue';
|
||||||
|
export { default as ApplicationModuleDeployView } from './ApplicationModuleDeployView.vue';
|
||||||
|
export { default as MintModuleDeploy } from './MintModuleDeploy.vue';
|
||||||
|
export { default as BurnModuleDeploy } from './BurnModuleDeploy.vue';
|
||||||
|
export { default as OracleModuleDeploy } from './OracleModuleDeploy.vue';
|
||||||
|
export { default as InheritanceModuleDeploy } from './InheritanceModuleDeploy.vue';
|
||||||
|
|
||||||
|
// Кастомный модуль
|
||||||
|
export { default as ModuleDeployFormView } from './ModuleDeployFormView.vue';
|
||||||
@@ -58,7 +58,7 @@ export default defineConfig({
|
|||||||
rewrite: (path) => path,
|
rewrite: (path) => path,
|
||||||
},
|
},
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'ws://dapp-backend:8000',
|
target: 'http://dapp-backend:8000',
|
||||||
ws: true,
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
|
||||||
if ! docker exec dapp-postgres pg_isready -U dapp_user -d dapp_db > /dev/null 2>&1; then
|
if ! docker exec dapp-postgres pg_isready -U dapp_user -d dapp_db > /dev/null 2>&1; then
|
||||||
|
|||||||
Reference in New Issue
Block a user