feat: Добавлены формы деплоя модулей DLE с полными настройками

- Создана форма деплоя TreasuryModule с детальными настройками казны
- Создана форма деплоя TimelockModule с настройками временных задержек
- Создана форма деплоя DLEReader с простой конфигурацией
- Добавлены маршруты и индексы для всех модулей
- Исправлены пути импорта BaseLayout
- Добавлены авторские права во все файлы
- Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
2025-09-23 02:57:59 +03:00
parent 9f94295d15
commit de0f8aecf2
63 changed files with 11631 additions and 1920 deletions

View File

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

View File

@@ -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
pragma solidity ^0.8.20;
/**
* @title MockNoop
* @dev Простой мок-контракт для тестирования FactoryDeployer
* @dev Простой мок-контракт для тестирования
*/
contract MockNoop {
uint256 public value;

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

View File

@@ -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
pragma solidity ^0.8.20;

View File

@@ -16,6 +16,41 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.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
* @dev Модуль казны для управления активами DLE
@@ -72,6 +107,11 @@ contract TreasuryModule is ReentrancyGuard {
// Система экстренного останова
bool public emergencyPaused;
address public emergencyAdmin;
// ERC-4337 Paymaster для оплаты газа любым токеном
address public paymaster;
mapping(address => bool) public gasPaymentTokens; // Токены, которыми можно платить за газ
mapping(address => uint256) public gasTokenRates; // Курсы обмена токенов на нативную монету
// События
event TokenAdded(
@@ -103,6 +143,10 @@ contract TreasuryModule is ReentrancyGuard {
);
event EmergencyPauseToggled(bool isPaused, address admin);
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() {
@@ -143,6 +187,11 @@ contract TreasuryModule is ReentrancyGuard {
*/
receive() external payable {
if (msg.value > 0) {
// Автоматически добавляем нативную монету, если её нет
if (!supportedTokens[address(0)].isActive) {
_addNativeToken();
}
_updateTokenBalance(address(0), supportedTokens[address(0)].balance + msg.value);
emit FundsDeposited(address(0), msg.sender, msg.value, supportedTokens[address(0)].balance);
}
@@ -373,6 +422,169 @@ contract TreasuryModule is ReentrancyGuard {
emergencyPaused = !emergencyPaused;
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 ФУНКЦИИ =====
@@ -396,17 +608,28 @@ contract TreasuryModule is ReentrancyGuard {
function getActiveTokens() external view returns (address[] memory) {
uint256 activeCount = 0;
// Считаем активные токены
// Считаем активные токены (включая нативную монету)
for (uint256 i = 0; i < tokenList.length; i++) {
if (supportedTokens[tokenList[i]].isActive) {
activeCount++;
}
}
// Нативная монета всегда активна
if (address(this).balance > 0 || supportedTokens[address(0)].isActive) {
activeCount++;
}
// Создаём массив активных токенов
address[] memory activeTokens = new address[](activeCount);
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++) {
if (supportedTokens[tokenList[i]].isActive) {
activeTokens[index] = tokenList[i];
@@ -421,6 +644,10 @@ contract TreasuryModule is ReentrancyGuard {
* @dev Получить баланс токена
*/
function getTokenBalance(address tokenAddress) external view returns (uint256) {
// Для нативной монеты возвращаем реальный баланс, если токен не зарегистрирован
if (tokenAddress == address(0) && !supportedTokens[address(0)].isActive) {
return address(this).balance;
}
return supportedTokens[tokenAddress].balance;
}
@@ -439,6 +666,10 @@ contract TreasuryModule is ReentrancyGuard {
* @dev Проверить, поддерживается ли токен
*/
function isTokenSupported(address tokenAddress) external view returns (bool) {
// Нативная монета всегда поддерживается
if (tokenAddress == address(0)) {
return true;
}
return supportedTokens[tokenAddress].isActive;
}
@@ -449,13 +680,25 @@ contract TreasuryModule is ReentrancyGuard {
uint256 totalTokens,
uint256 totalTxs,
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 (
totalTokensSupported,
totalTransactions,
chainId,
emergencyPaused
emergencyPaused,
paymaster,
gasTokensCount
);
}

View File

@@ -34,7 +34,7 @@ let pool = new Pool({
max: 10, // Максимальное количество клиентов в пуле
min: 0, // Минимальное количество клиентов в пуле
idleTimeoutMillis: 30000, // Время жизни неактивного клиента (30 сек)
connectionTimeoutMillis: 2000, // Таймаут подключения (2 сек)
connectionTimeoutMillis: 30000, // Таймаут подключения (30 сек)
maxUses: 7500, // Максимальное количество использований клиента
allowExitOnIdle: true, // Разрешить выход при отсутствии активных клиентов
});

View File

@@ -15,23 +15,9 @@ require('hardhat-contract-sizer');
require('dotenv').config();
function getNetworks() {
const supported = [
{ id: 'bsc', envUrl: 'BSC_RPC_URL', envKey: 'BSC_PRIVATE_KEY' },
{ id: 'ethereum', envUrl: 'ETHEREUM_RPC_URL', envKey: 'ETHEREUM_PRIVATE_KEY' },
{ 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;
// Возвращаем пустой объект, чтобы Hardhat не зависел от переменных окружения
// Сети будут настраиваться динамически в deploy-multichain.js
return {};
}
module.exports = {

View File

@@ -21,7 +21,6 @@
"format:check": "prettier --check \"**/*.{js,vue,json,md}\"",
"run-migrations": "node scripts/run-migrations.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:complete": "node scripts/deploy/deploy-dle-complete.js"
},

View File

@@ -65,7 +65,8 @@ router.post('/read-dle-info', async (req, res) => {
"function balanceOf(address account) external view returns (uint256)",
"function quorumPercentage() 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);
@@ -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 = {
name: dleInfo.name,
symbol: dleInfo.symbol,
@@ -193,7 +224,8 @@ router.post('/read-dle-info', async (req, res) => {
quorumPercentage: Number(quorumPercentage),
currentChainId: Number(currentChainId),
rpcUsed: rpcUrl,
participantCount: participantCount
participantCount: participantCount,
modules: modules // Информация о модулях
};
console.log(`[Blockchain] Данные DLE прочитаны из блокчейна:`, blockchainData);
@@ -212,92 +244,7 @@ router.post('/read-dle-info', async (req, res) => {
}
});
// Получение поддерживаемых сетей из смарт-контракта
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
});
}
});
// УДАЛЕНО: дублируется в dleMultichain.js
// Получение списка всех предложений
router.post('/get-proposals', async (req, res) => {
@@ -354,7 +301,7 @@ router.post('/get-proposals', async (req, res) => {
// Пробуем несколько раз для новых предложений
let proposal, isPassed;
let retryCount = 0;
const maxRetries = 3;
const maxRetries = 1;
while (retryCount < maxRetries) {
try {
@@ -708,111 +655,9 @@ router.post('/load-deactivation-proposals', async (req, res) => {
}
});
// Создать предложение о добавлении модуля
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: 'Все поля обязательны'
});
}
// УДАЛЕНО: дублируется в dleModules.js
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 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
});
}
});
// УДАЛЕНО: дублируется в dleModules.js
// УДАЛЯЕМ эту функцию - создание предложений выполняется только через frontend с MetaMask
// router.post('/create-proposal', ...) - УДАЛЕНО
@@ -925,264 +770,15 @@ router.post('/cancel-proposal', async (req, res) => {
}
});
// Проверить подключение к сети
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: 'Все поля обязательны'
});
}
// УДАЛЕНО: дублируется в dleMultichain.js
console.log(`[Blockchain] Проверка подключения к сети ${chainId} для DLE: ${dleAddress}`);
// УДАЛЕНО: дублируется в dleMultichain.js
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
if (!rpcUrl) {
return res.status(500).json({
success: false,
error: 'RPC URL для Sepolia не найден'
});
}
// УДАЛЕНО: дублируется в dleMultichain.js
const provider = new ethers.JsonRpcProvider(rpcUrl);
const dleAbi = [
"function checkChainConnection(uint256 _chainId) public view returns (bool isAvailable)"
];
// УДАЛЕНО: дублируется в dleMultichain.js
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
// Проверяем подключение
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
});
}
});
// УДАЛЕНО: дублируется в dleMultichain.js
// Получить параметры управления
router.post('/get-governance-params', async (req, res) => {
@@ -1707,139 +1303,11 @@ router.post('/is-active', async (req, res) => {
}
});
// Проверить активность модуля
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 модуля обязательны'
});
}
// УДАЛЕНО: дублируется в dleModules.js
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 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
});
}
});
// УДАЛЕНО: дублируется в dleModules.js
// Получить аналитику DLE
router.post('/get-dle-analytics', async (req, res) => {

View File

@@ -42,7 +42,7 @@ router.post('/read-dle-info', async (req, res) => {
// ABI для чтения данных DLE
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 balanceOf(address account) 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,
coordinates: dleInfo.coordinates,
jurisdiction: Number(dleInfo.jurisdiction),
oktmo: Number(dleInfo.oktmo),
okvedCodes: dleInfo.okvedCodes,
kpp: Number(dleInfo.kpp),
creationTimestamp: Number(dleInfo.creationTimestamp),

File diff suppressed because it is too large Load Diff

View File

@@ -40,13 +40,21 @@ router.post('/get-supported-chains', async (req, res) => {
const provider = new ethers.JsonRpcProvider(rpcUrl);
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 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);

View File

@@ -42,9 +42,11 @@ router.post('/get-proposals', async (req, res) => {
// ABI для чтения предложений (используем правильные функции из смарт-контракта)
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 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)"
];
@@ -68,15 +70,34 @@ router.post('/get-proposals', async (req, res) => {
console.log(`[DLE Proposals] Читаем предложение ID: ${proposalId}`);
// Пробуем несколько раз для новых предложений
let proposal, isPassed;
let proposalState, isPassed, quorumReached, forVotes, againstVotes, quorumRequired;
let retryCount = 0;
const maxRetries = 3;
const maxRetries = 1;
while (retryCount < maxRetries) {
try {
proposal = await dle.getProposalSummary(proposalId);
proposalState = await dle.getProposalState(proposalId);
const result = await dle.checkProposalResult(proposalId);
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; // Успешно прочитали
} catch (error) {
retryCount++;
@@ -90,33 +111,29 @@ router.post('/get-proposals', async (req, res) => {
}
console.log(`[DLE Proposals] Данные предложения ${proposalId}:`, {
id: Number(proposal.id),
description: proposal.description,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
executed: proposal.executed,
canceled: proposal.canceled,
deadline: Number(proposal.deadline),
initiator: proposal.initiator,
governanceChainId: Number(proposal.governanceChainId),
snapshotTimepoint: Number(proposal.snapshotTimepoint),
targets: proposal.targets
id: Number(proposalId),
description: events[i].args.description,
state: Number(proposalState),
isPassed: isPassed,
quorumReached: quorumReached,
forVotes: Number(forVotes),
againstVotes: Number(againstVotes),
quorumRequired: Number(quorumRequired),
initiator: events[i].args.initiator
});
const proposalInfo = {
id: Number(proposal.id),
description: proposal.description,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
executed: proposal.executed,
canceled: proposal.canceled,
deadline: Number(proposal.deadline),
initiator: proposal.initiator,
governanceChainId: Number(proposal.governanceChainId),
snapshotTimepoint: Number(proposal.snapshotTimepoint),
targetChains: proposal.targets.map(chainId => Number(chainId)),
id: Number(proposalId),
description: events[i].args.description,
state: Number(proposalState),
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);
@@ -182,29 +199,40 @@ router.post('/get-proposal-info', async (req, res) => {
// ABI для чтения информации о предложении
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)"
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
"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 proposal = await dle.proposals(proposalId);
const isPassed = await dle.checkProposalResult(proposalId);
// Ищем событие ProposalCreated для этого предложения
const currentBlock = await provider.getBlockNumber();
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 = {
description: proposal.description,
duration: Number(proposal.duration),
operation: proposal.operation,
governanceChainId: Number(proposal.governanceChainId),
startTime: Number(proposal.startTime),
executed: proposal.executed,
forVotes: Number(proposal.forVotes),
againstVotes: Number(proposal.againstVotes),
isPassed: isPassed
id: Number(proposalId),
description: proposalEvent.args.description,
initiator: proposalEvent.args.initiator,
blockNumber: proposalEvent.blockNumber,
transactionHash: proposalEvent.transactionHash,
state: Number(state),
isPassed: result.passed,
quorumReached: result.quorumReached
};
console.log(`[DLE Proposals] Информация о предложении получена:`, proposalInfo);
@@ -300,24 +328,30 @@ router.post('/get-proposal-votes', async (req, res) => {
const provider = new ethers.JsonRpcProvider(rpcUrl);
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 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({
success: true,
data: {
proposalId: Number(proposalId),
forVotes: Number(votes.forVotes),
againstVotes: Number(votes.againstVotes),
totalVotes: Number(votes.totalVotes),
quorumRequired: Number(votes.quorumRequired)
isPassed: result.passed,
quorumReached: result.quorumReached,
state: Number(state),
// Пока не можем получить точные голоса, так как функция не существует в контракте
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) => {
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({
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);
if (!rpcUrl) {
@@ -562,32 +599,34 @@ router.post('/execute-proposal', async (req, res) => {
}
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(privateKey, provider);
const dleAbi = [
"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 receipt = await tx.wait();
// Подготавливаем данные для транзакции (не отправляем)
const txData = await dle.executeProposal.populateTransaction(proposalId);
console.log(`[DLE Proposals] Предложение исполнено:`, receipt);
console.log(`[DLE Proposals] Данные транзакции исполнения подготовлены:`, txData);
res.json({
success: true,
data: {
transactionHash: receipt.hash
to: dleAddress,
data: txData.data,
value: "0x0",
gasLimit: "0x1e8480", // 2,000,000 gas
message: `Подготовлены данные для исполнения предложения ${proposalId}. Отправьте транзакцию через MetaMask.`
}
});
} catch (error) {
console.error('[DLE Proposals] Ошибка при исполнении предложения:', error);
console.error('[DLE Proposals] Ошибка при подготовке исполнения предложения:', error);
res.status(500).json({
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;

View File

@@ -18,10 +18,36 @@ const auth = require('../middleware/auth');
const path = require('path');
const fs = require('fs');
const ethers = require('ethers'); // Added ethers for private key validation
const deploymentTracker = require('../utils/deploymentTracker');
const create2 = require('../utils/create2');
const verificationStore = require('../services/verificationStore');
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
* @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) => {
try {
const dleParams = req.body;
logger.info('Получен запрос на создание DLE v2:', dleParams);
logger.info('🔥 Получен запрос на асинхронный деплой DLE v2');
// Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя
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({
success: true,
message: 'DLE v2 успешно создано',
data: result.data
message: 'Деплой запущен в фоновом режиме',
deploymentId: deploymentId
});
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
logger.error('Ошибка при запуске асинхронного деплоя:', error);
res.status(500).json({
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
@@ -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;
/**
* Дополнительные маршруты (подключаются из 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 верификации (если нужно отдельным вызовом)
router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => {

View 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
*/
/**
* ENS utilities: resolve avatar URL for a given ENS name
*/

View File

@@ -17,6 +17,32 @@ const logger = require('../utils/logger');
const { ethers } = require('ethers');
const db = require('../db');
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 aiProviderSettingsService = require('../services/aiProviderSettingsService');
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',
[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) {
// Для админов возвращаем полные данные

View 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
*/
/**
* Загрузка файлов (логотипы) через Multer
*/

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

View File

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

View File

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

View File

@@ -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
View 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 */
const hre = require('hardhat');
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)}`);
}
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) {
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
};
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`);
const txFill = await wallet.sendTransaction(txReq);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`);
await txFill.wait();
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`);
sent = true;
} catch (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)} ready for DLE deployment with nonce=${current}`);
} else {
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;
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);
console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`);
const initTx = await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress);
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++;
} else {
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) {
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);
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++) {
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 });
}
// ПАРАЛЛЕЛЬНЫЙ деплой во всех сетях одновременно
console.log(`[MULTI_DBG] Starting PARALLEL deployment to ${networks.length} networks`);
const deploymentPromises = networks.map(async (rpcUrl, i) => {
console.log(`[MULTI_DBG] 🚀 Starting deployment to network ${i + 1}/${networks.length}: ${rpcUrl}`);
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)];
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) {
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');
}
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] 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);
// Деплой модулей ОТКЛЮЧЕН - модули будут деплоиться отдельно
console.log('[MULTI_DBG] Module deployment DISABLED - modules will be deployed separately');
const moduleResults = [];
const verificationResults = [];
// Объединяем результаты
const finalResults = results.map((result, index) => ({
@@ -554,62 +621,62 @@ async function main() {
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()
};
// Сохраняем каждый модуль в отдельный файл
const dleAddress = uniqueAddresses[0];
const modulesDir = path.join(__dirname, '../contracts-data/modules');
if (!fs.existsSync(modulesDir)) {
fs.mkdirSync(modulesDir, { recursive: true });
}
// Получаем 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);
// Создаем файлы для каждого типа модуля
const moduleTypes = ['treasury', 'timelock', 'reader'];
const moduleKeys = ['treasuryModule', 'timelockModule', 'dleReader'];
for (let moduleIndex = 0; moduleIndex < moduleTypes.length; moduleIndex++) {
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
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}`;
try {
const provider = new hre.ethers.JsonRpcProvider(rpcUrl);
const network = await provider.getNetwork();
moduleInfo.networks.push({
chainId: Number(network.chainId),
rpcUrl: rpcUrl,
address: moduleResult && moduleResult[moduleKey] ? moduleResult[moduleKey] : null,
verification: verificationResults[i] && verificationResults[i][moduleKey] ? verificationResults[i][moduleKey] : 'unknown'
});
} catch (error) {
console.error(`[MULTI_DBG] Ошибка получения chainId для модуля ${moduleType} в сети ${i + 1}:`, error.message);
moduleInfo.networks.push({
chainId: null,
rpcUrl: rpcUrl,
address: null,
verification: 'error'
});
}
}
// Сохраняем файл модуля
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 если её нет
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}`);
console.log(`[MULTI_DBG] All modules saved to separate files in: ${modulesDir}`);
}
main().catch((e) => { console.error(e); process.exit(1); });

View File

@@ -66,15 +66,22 @@ initWSS(server);
async function startServer() {
await initDbPool(); // Дождаться пересоздания пула!
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
// Инициализация AI ассистента В ФОНЕ (неблокирующая)
seedAIAssistantSettings().catch(error => {
console.warn('[Server] Ollama недоступен, AI ассистент будет инициализирован позже:', error.message);
});
// Разогрев модели Ollama
// console.log('🔥 Запуск разогрева модели...');
setTimeout(() => {
}, 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 () => {

View 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
*/
/**
* Сервис для динамического управления подключениями к базе данных
* Позволяет изменять настройки БД без перезапуска приложения

View File

@@ -16,6 +16,7 @@ const fs = require('fs');
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const { getRpcUrlByChainId } = require('./rpcProviderService');
const deploymentTracker = require('../utils/deploymentTracker');
const etherscanV2 = require('./etherscanV2VerificationService');
const verificationStore = require('./verificationStore');
@@ -29,11 +30,18 @@ class DLEV2Service {
* @param {Object} dleParams - Параметры 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 tempParamsFile = null;
try {
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
// WebSocket обновление: начало процесса
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Валидация параметров', 5, 'Проверяем входные данные');
}
// Валидация входных данных
this.validateDLEParams(dleParams);
@@ -50,6 +58,11 @@ class DLEV2Service {
logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message);
}
// WebSocket обновление: генерация CREATE2_SALT
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Генерация CREATE2 SALT', 10, 'Создаем уникальный идентификатор для детерминированного адреса');
}
// Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets
const { createAndStoreNewCreate2Salt } = require('./secretStore');
const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' });
@@ -66,6 +79,11 @@ class DLEV2Service {
}
fs.copyFileSync(paramsFile, tempParamsFile);
// WebSocket обновление: поиск RPC URLs
if (deploymentId) {
deploymentTracker.updateProgress(deploymentId, 'Поиск RPC endpoints', 15, 'Подключаемся к блокчейн сетям');
}
// Готовим RPC для всех выбранных сетей
const rpcUrls = [];
for (const cid of deployParams.supportedChainIds) {
@@ -99,14 +117,7 @@ class DLEV2Service {
const walletAddress = new ethers.Wallet(pk, provider).address;
const balance = await provider.getBalance(walletAddress);
if (typeof ethers.parseEther !== 'function') {
throw new Error('Метод ethers.parseEther не найден');
}
const minBalance = ethers.parseEther("0.00001");
if (typeof ethers.formatEther !== 'function') {
throw new Error('Метод ethers.formatEther не найден');
}
logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`);
if (balance < minBalance) {
throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`);
@@ -117,27 +128,87 @@ class DLEV2Service {
throw new Error('Приватный ключ для деплоя не передан');
}
// Рассчитываем INIT_CODE_HASH автоматически из актуального initCode
const initCodeHash = await this.computeInitCodeHash({
...deployParams,
currentChainId: deployParams.currentChainId || deployParams.supportedChainIds[0]
});
// Сохраняем ключ Etherscan V2 ПЕРЕД деплоем
logger.info(`🔑 Etherscan API Key получен: ${dleParams.etherscanApiKey ? '[ЕСТЬ]' : '[НЕТ]'}`);
try {
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 напрямую
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("🔍 DEBUG: Подготовка к прямому деплою...");
const result = await this.runDeployMultichain(paramsFile, {
rpcUrls: rpcUrls,
chainIds: deployParams.supportedChainIds,
privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`,
salt: create2Salt,
initCodeHash
etherscanApiKey: dleParams.etherscanApiKey
});
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 для отображения на странице управления
try {
@@ -148,6 +219,7 @@ class DLEV2Service {
logger.error('Неверная структура результата деплоя:', result);
throw new Error('Неверная структура результата деплоя');
}
logger.info("🔍 DEBUG: Вызываем runDeployMultichain...");
// Если результат - массив (прямой результат из скрипта), преобразуем его
let deployResult = result;
@@ -209,6 +281,14 @@ class DLEV2Service {
fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2));
// 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 {
success: true,
data: dleData
@@ -220,31 +300,25 @@ class DLEV2Service {
logger.warn('Не удалось сохранить локальную карточку DLE:', e.message);
}
// Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан
try {
if (dleParams.etherscanApiKey) {
const { setSecret } = require('./secretStore');
await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey);
}
} catch (_) {}
// Etherscan API Key уже сохранен в начале функции
// Авто-верификация через Etherscan V2 (опционально)
if (dleParams.autoVerifyAfterDeploy) {
try {
await this.autoVerifyAcrossChains({
deployParams,
deployResult: result,
apiKey: dleParams.etherscanApiKey
});
} catch (e) {
logger.warn('Авто-верификация завершилась с ошибкой:', e.message);
}
// Верификация выполняется в deploy-multichain.js
// WebSocket обновление: деплой успешно завершен
if (deploymentId) {
deploymentTracker.completeDeployment(deploymentId, result);
}
return result;
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
// WebSocket обновление: деплой завершился с ошибкой
if (deploymentId) {
deploymentTracker.failDeployment(deploymentId, error);
}
throw error;
} finally {
try {
@@ -423,9 +497,6 @@ class DLEV2Service {
// Принимаем как строки, так и числа; конвертируем в base units (18 знаков)
try {
if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) {
if (typeof ethers.parseUnits !== 'function') {
throw new Error('Метод ethers.parseUnits не найден');
}
return ethers.parseUnits(rawAmount.toString(), 18).toString();
}
if (typeof rawAmount === 'string') {
@@ -435,9 +506,6 @@ class DLEV2Service {
return BigInt(a).toString();
}
// Десятичная строка — конвертируем в base units
if (typeof ethers.parseUnits !== 'function') {
throw new Error('Метод ethers.parseUnits не найден');
}
return ethers.parseUnits(a, 18).toString();
}
// BigInt или иные типы — приводим к строке без изменения масштаба
@@ -530,7 +598,7 @@ class DLEV2Service {
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '';
@@ -575,13 +643,19 @@ class DLEV2Service {
const envVars = {
...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], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
stdio: ['inherit', 'pipe', 'pipe']
});
let stdout = '', stderr = '';
@@ -838,11 +912,11 @@ class DLEV2Service {
// Преобразуем группы в массив
return Array.from(groups.values()).map(group => ({
...group,
// Основной адрес DLE (из первой сети)
dleAddress: group.networks[0]?.dleAddress,
...group,
// Основной адрес DLE (из первой сети)
dleAddress: group.networks[0]?.dleAddress,
// Общее количество сетей
totalNetworks: group.networks.length,
totalNetworks: group.networks.length,
// Поддерживаемые сети
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 balance = await provider.getBalance(wallet.address);
if (typeof ethers.formatEther !== 'function') {
throw new Error('Метод ethers.formatEther не найден');
}
const balanceEth = ethers.formatEther(balance);
if (typeof ethers.parseEther !== 'function') {
throw new Error('Метод ethers.parseEther не найден');
}
const minBalance = ethers.parseEther("0.001");
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();

View File

@@ -585,7 +585,7 @@ class EmailBotService {
}
async sendEmail(to, subject, text) {
const maxRetries = 3;
const maxRetries = 1;
const retryDelay = 5000; // 5 секунд между попытками
for (let attempt = 1; attempt <= maxRetries; attempt++) {

View 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
*/
/**
* Lightweight encrypted secret store over encryptedDatabaseService
*/

View 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
*/
/**
* Сервис получения балансов токенов пользователя из БД и RPC
*/

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

View 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
*/
const { keccak256, getAddress } = require('ethers').utils || require('ethers');
function toBytes(hex) {

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

View File

@@ -12,6 +12,7 @@
const WebSocket = require('ws');
const tokenBalanceService = require('./services/tokenBalanceService');
const deploymentTracker = require('./utils/deploymentTracker');
let wss = null;
// Храним клиентов по userId для персонализированных уведомлений
@@ -28,6 +29,11 @@ const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
function initWSS(server) {
wss = new WebSocket.Server({ server, path: '/ws' });
// Подключаем deployment tracker к WebSocket
deploymentTracker.on('deployment_updated', (data) => {
broadcastDeploymentUpdate(data);
});
wss.on('connection', (ws, req) => {
// console.log('🔌 [WebSocket] Новое подключение');
// 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 = {
initWSS,
broadcastContactsUpdate,
@@ -469,6 +498,7 @@ module.exports = {
broadcastAuthTokenUpdated,
broadcastTokenBalancesUpdate,
broadcastTokenBalanceChanged,
broadcastDeploymentUpdate,
getConnectedUsers,
getStats
};