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

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