feat: Добавлены формы деплоя модулей DLE с полными настройками
- Создана форма деплоя TreasuryModule с детальными настройками казны - Создана форма деплоя TimelockModule с настройками временных задержек - Создана форма деплоя DLEReader с простой конфигурацией - Добавлены маршруты и индексы для всех модулей - Исправлены пути импорта BaseLayout - Добавлены авторские права во все файлы - Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
286
backend/utils/NonceManager.js
Normal file
286
backend/utils/NonceManager.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const { ethers } = require('ethers');
|
||||
|
||||
/**
|
||||
* Менеджер nonce для синхронизации транзакций в мультичейн-деплое
|
||||
* Обеспечивает правильную последовательность транзакций без конфликтов
|
||||
*/
|
||||
class NonceManager {
|
||||
constructor() {
|
||||
this.nonceCache = new Map(); // Кэш nonce для каждого кошелька
|
||||
this.pendingTransactions = new Map(); // Ожидающие транзакции
|
||||
this.locks = new Map(); // Блокировки для предотвращения конкурентного доступа
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить актуальный nonce для кошелька в сети
|
||||
* @param {string} rpcUrl - URL RPC провайдера
|
||||
* @param {string} walletAddress - Адрес кошелька
|
||||
* @param {boolean} usePending - Использовать pending транзакции
|
||||
* @returns {Promise<number>} Актуальный nonce
|
||||
*/
|
||||
async getCurrentNonce(rpcUrl, walletAddress, usePending = true) {
|
||||
const key = `${walletAddress}-${rpcUrl}`;
|
||||
|
||||
try {
|
||||
// Создаем провайдер из rpcUrl
|
||||
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { staticNetwork: true });
|
||||
|
||||
const nonce = await Promise.race([
|
||||
provider.getTransactionCount(walletAddress, usePending ? 'pending' : 'latest'),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('Nonce timeout')), 30000))
|
||||
]);
|
||||
|
||||
console.log(`[NonceManager] Получен nonce для ${walletAddress} в сети ${rpcUrl}: ${nonce}`);
|
||||
return nonce;
|
||||
} catch (error) {
|
||||
console.error(`[NonceManager] Ошибка получения nonce для ${walletAddress}:`, error.message);
|
||||
|
||||
// Если сеть недоступна, возвращаем 0 как fallback
|
||||
if (error.message.includes('network is not available') || error.message.includes('NETWORK_ERROR')) {
|
||||
console.warn(`[NonceManager] Сеть недоступна, используем nonce 0 для ${walletAddress}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Заблокировать nonce для транзакции
|
||||
* @param {ethers.Wallet} wallet - Кошелек
|
||||
* @param {ethers.Provider} provider - Провайдер сети
|
||||
* @returns {Promise<number>} Заблокированный nonce
|
||||
*/
|
||||
async lockNonce(rpcUrl, walletAddress) {
|
||||
const key = `${walletAddress}-${rpcUrl}`;
|
||||
|
||||
// Ждем освобождения блокировки
|
||||
while (this.locks.has(key)) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Устанавливаем блокировку
|
||||
this.locks.set(key, true);
|
||||
|
||||
try {
|
||||
const currentNonce = await this.getCurrentNonce(rpcUrl, walletAddress);
|
||||
const lockedNonce = currentNonce;
|
||||
|
||||
// Обновляем кэш
|
||||
this.nonceCache.set(key, lockedNonce + 1);
|
||||
|
||||
console.log(`[NonceManager] Заблокирован nonce ${lockedNonce} для ${walletAddress} в сети ${rpcUrl}`);
|
||||
return lockedNonce;
|
||||
} finally {
|
||||
// Освобождаем блокировку
|
||||
this.locks.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Освободить nonce после успешной транзакции
|
||||
* @param {ethers.Wallet} wallet - Кошелек
|
||||
* @param {ethers.Provider} provider - Провайдер сети
|
||||
* @param {number} nonce - Использованный nonce
|
||||
*/
|
||||
releaseNonce(rpcUrl, walletAddress, nonce) {
|
||||
const key = `${walletAddress}-${rpcUrl}`;
|
||||
const cachedNonce = this.nonceCache.get(key) || 0;
|
||||
|
||||
if (nonce >= cachedNonce) {
|
||||
this.nonceCache.set(key, nonce + 1);
|
||||
}
|
||||
|
||||
console.log(`[NonceManager] Освобожден nonce ${nonce} для ${walletAddress} в сети ${rpcUrl}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Синхронизировать nonce между сетями
|
||||
* @param {Array} networks - Массив сетей с кошельками
|
||||
* @returns {Promise<number>} Синхронизированный nonce
|
||||
*/
|
||||
async synchronizeNonce(networks) {
|
||||
console.log(`[NonceManager] Начинаем синхронизацию nonce для ${networks.length} сетей`);
|
||||
|
||||
// Получаем nonce для всех сетей
|
||||
const nonces = await Promise.all(
|
||||
networks.map(async (network, index) => {
|
||||
try {
|
||||
const nonce = await this.getCurrentNonce(network.rpcUrl, network.wallet.address);
|
||||
console.log(`[NonceManager] Сеть ${index + 1}/${networks.length} (${network.chainId}): nonce=${nonce}`);
|
||||
return { chainId: network.chainId, nonce, index };
|
||||
} catch (error) {
|
||||
console.error(`[NonceManager] Ошибка получения nonce для сети ${network.chainId}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Находим максимальный nonce
|
||||
const maxNonce = Math.max(...nonces.map(n => n.nonce));
|
||||
console.log(`[NonceManager] Максимальный nonce: ${maxNonce}`);
|
||||
|
||||
// Выравниваем nonce во всех сетях
|
||||
for (const network of networks) {
|
||||
const currentNonce = nonces.find(n => n.chainId === network.chainId)?.nonce || 0;
|
||||
|
||||
if (currentNonce < maxNonce) {
|
||||
console.log(`[NonceManager] Выравниваем nonce в сети ${network.chainId} с ${currentNonce} до ${maxNonce}`);
|
||||
await this.alignNonce(network.wallet, network.provider, currentNonce, maxNonce);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[NonceManager] Синхронизация nonce завершена. Целевой nonce: ${maxNonce}`);
|
||||
return maxNonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выровнять nonce до целевого значения
|
||||
* @param {ethers.Wallet} wallet - Кошелек
|
||||
* @param {ethers.Provider} provider - Провайдер сети
|
||||
* @param {number} currentNonce - Текущий nonce
|
||||
* @param {number} targetNonce - Целевой nonce
|
||||
*/
|
||||
async alignNonce(wallet, provider, currentNonce, targetNonce) {
|
||||
const burnAddress = "0x000000000000000000000000000000000000dEaD";
|
||||
let nonce = currentNonce;
|
||||
|
||||
while (nonce < targetNonce) {
|
||||
try {
|
||||
// Получаем актуальный nonce перед каждой транзакцией
|
||||
const actualNonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
|
||||
if (actualNonce > nonce) {
|
||||
nonce = actualNonce;
|
||||
continue;
|
||||
}
|
||||
|
||||
const feeOverrides = await this.getFeeOverrides(provider);
|
||||
const txReq = {
|
||||
to: burnAddress,
|
||||
value: 0n,
|
||||
nonce: nonce,
|
||||
gasLimit: 21000,
|
||||
...feeOverrides
|
||||
};
|
||||
|
||||
console.log(`[NonceManager] Отправляем заполняющую транзакцию nonce=${nonce} в сети ${provider._network?.chainId}`);
|
||||
const tx = await wallet.sendTransaction(txReq);
|
||||
await tx.wait();
|
||||
|
||||
console.log(`[NonceManager] Заполняющая транзакция nonce=${nonce} подтверждена в сети ${provider._network?.chainId}`);
|
||||
nonce++;
|
||||
|
||||
// Небольшая задержка между транзакциями
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[NonceManager] Ошибка заполняющей транзакции nonce=${nonce}:`, error.message);
|
||||
|
||||
if (error.message.includes('nonce too low')) {
|
||||
// Обновляем nonce и пробуем снова
|
||||
nonce = await this.getCurrentNonce(provider._getConnection().url, wallet.address);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить параметры комиссии для сети
|
||||
* @param {ethers.Provider} provider - Провайдер сети
|
||||
* @returns {Promise<Object>} Параметры комиссии
|
||||
*/
|
||||
async getFeeOverrides(provider) {
|
||||
try {
|
||||
const feeData = await provider.getFeeData();
|
||||
|
||||
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
|
||||
return {
|
||||
maxFeePerGas: feeData.maxFeePerGas,
|
||||
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
gasPrice: feeData.gasPrice
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[NonceManager] Ошибка получения fee data:`, error.message);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Безопасная отправка транзакции с правильным nonce
|
||||
* @param {ethers.Wallet} wallet - Кошелек
|
||||
* @param {ethers.Provider} provider - Провайдер сети
|
||||
* @param {Object} txData - Данные транзакции
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @returns {Promise<ethers.TransactionResponse>} Результат транзакции
|
||||
*/
|
||||
async sendTransactionSafely(wallet, provider, txData, maxRetries = 1) {
|
||||
const rpcUrl = provider._getConnection().url;
|
||||
const walletAddress = wallet.address;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
// Получаем актуальный nonce
|
||||
const nonce = await this.lockNonce(rpcUrl, walletAddress);
|
||||
|
||||
const tx = await wallet.sendTransaction({
|
||||
...txData,
|
||||
nonce: nonce
|
||||
});
|
||||
|
||||
console.log(`[NonceManager] Транзакция отправлена с nonce=${nonce} в сети ${provider._network?.chainId}`);
|
||||
|
||||
// Ждем подтверждения
|
||||
await tx.wait();
|
||||
|
||||
// Освобождаем nonce
|
||||
this.releaseNonce(rpcUrl, walletAddress, nonce);
|
||||
|
||||
return tx;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[NonceManager] Попытка ${attempt + 1}/${maxRetries} неудачна:`, error.message);
|
||||
|
||||
if (error.message.includes('nonce too low') && attempt < maxRetries - 1) {
|
||||
// Обновляем nonce и пробуем снова
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attempt === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить кэш nonce
|
||||
*/
|
||||
clearCache() {
|
||||
this.nonceCache.clear();
|
||||
this.pendingTransactions.clear();
|
||||
this.locks.clear();
|
||||
console.log(`[NonceManager] Кэш nonce очищен`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NonceManager;
|
||||
@@ -1,3 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const { keccak256, getAddress } = require('ethers').utils || require('ethers');
|
||||
|
||||
function toBytes(hex) {
|
||||
|
||||
239
backend/utils/deploymentTracker.js
Normal file
239
backend/utils/deploymentTracker.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class DeploymentTracker extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.deployments = new Map(); // В продакшене использовать Redis
|
||||
this.logger = require('../utils/logger');
|
||||
}
|
||||
|
||||
// Создать новый деплой
|
||||
createDeployment(params) {
|
||||
const deploymentId = this.generateDeploymentId();
|
||||
|
||||
const deployment = {
|
||||
id: deploymentId,
|
||||
status: 'pending',
|
||||
stage: 'initializing',
|
||||
progress: 0,
|
||||
startedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
params,
|
||||
networks: {},
|
||||
logs: [],
|
||||
result: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
this.deployments.set(deploymentId, deployment);
|
||||
this.logger.info(`📝 Создан новый деплой: ${deploymentId}`);
|
||||
|
||||
return deploymentId;
|
||||
}
|
||||
|
||||
// Получить статус деплоя
|
||||
getDeployment(deploymentId) {
|
||||
return this.deployments.get(deploymentId);
|
||||
}
|
||||
|
||||
// Обновить статус деплоя
|
||||
updateDeployment(deploymentId, updates) {
|
||||
const deployment = this.deployments.get(deploymentId);
|
||||
if (!deployment) {
|
||||
this.logger.error(`❌ Деплой не найден: ${deploymentId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(deployment, updates, { updatedAt: new Date() });
|
||||
this.deployments.set(deploymentId, deployment);
|
||||
|
||||
// Отправляем событие через WebSocket
|
||||
this.emit('deployment_updated', {
|
||||
deploymentId,
|
||||
...updates
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Добавить лог
|
||||
addLog(deploymentId, message, type = 'info') {
|
||||
const deployment = this.deployments.get(deploymentId);
|
||||
if (!deployment) return false;
|
||||
|
||||
const logEntry = {
|
||||
timestamp: new Date(),
|
||||
message,
|
||||
type
|
||||
};
|
||||
|
||||
deployment.logs.push(logEntry);
|
||||
deployment.updatedAt = new Date();
|
||||
|
||||
// Отправляем только лог через WebSocket (без дублирования)
|
||||
this.emit('deployment_updated', {
|
||||
deploymentId,
|
||||
type: 'deployment_log',
|
||||
log: logEntry
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обновить статус сети
|
||||
updateNetworkStatus(deploymentId, network, status, address = null, message = null) {
|
||||
const deployment = this.deployments.get(deploymentId);
|
||||
if (!deployment) return false;
|
||||
|
||||
deployment.networks[network] = {
|
||||
status,
|
||||
address,
|
||||
message,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
deployment.updatedAt = new Date();
|
||||
|
||||
// Отправляем обновление через WebSocket
|
||||
this.emit('deployment_updated', {
|
||||
deploymentId,
|
||||
type: 'deployment_network_update',
|
||||
network,
|
||||
status,
|
||||
address,
|
||||
message
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обновить прогресс
|
||||
updateProgress(deploymentId, stage, progress, message = null) {
|
||||
const updates = {
|
||||
stage,
|
||||
progress,
|
||||
status: progress >= 100 ? 'completed' : 'in_progress'
|
||||
};
|
||||
|
||||
// Обновляем без отправки события (только внутреннее обновление)
|
||||
const deployment = this.deployments.get(deploymentId);
|
||||
if (deployment) {
|
||||
Object.assign(deployment, updates, { updatedAt: new Date() });
|
||||
this.deployments.set(deploymentId, deployment);
|
||||
}
|
||||
|
||||
// Лог добавляется через updateDeployment, не дублируем событие
|
||||
}
|
||||
|
||||
// Завершить деплой успешно
|
||||
completeDeployment(deploymentId, result) {
|
||||
const updates = {
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
result,
|
||||
completedAt: new Date()
|
||||
};
|
||||
|
||||
this.updateDeployment(deploymentId, updates);
|
||||
|
||||
// Событие уже отправлено через updateDeployment
|
||||
|
||||
this.logger.info(`✅ Деплой завершен: ${deploymentId}`);
|
||||
}
|
||||
|
||||
// Завершить деплой с ошибкой
|
||||
failDeployment(deploymentId, error) {
|
||||
const updates = {
|
||||
status: 'failed',
|
||||
error: error.message || error,
|
||||
failedAt: new Date()
|
||||
};
|
||||
|
||||
this.updateDeployment(deploymentId, updates);
|
||||
|
||||
// Событие уже отправлено через updateDeployment
|
||||
|
||||
this.logger.error(`❌ Деплой провален: ${deploymentId}`, error);
|
||||
}
|
||||
|
||||
// Очистить старые деплои (вызывать по крону)
|
||||
cleanupOldDeployments(olderThanHours = 24) {
|
||||
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
|
||||
let cleaned = 0;
|
||||
|
||||
for (const [id, deployment] of this.deployments.entries()) {
|
||||
if (deployment.updatedAt < cutoff && ['completed', 'failed'].includes(deployment.status)) {
|
||||
this.deployments.delete(id);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned > 0) {
|
||||
this.logger.info(`🧹 Очищено ${cleaned} старых деплоев`);
|
||||
}
|
||||
}
|
||||
|
||||
// Сгенерировать уникальный ID
|
||||
generateDeploymentId() {
|
||||
return `deploy_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
||||
}
|
||||
|
||||
// Получить все активные деплои
|
||||
getActiveDeployments() {
|
||||
const active = [];
|
||||
for (const deployment of this.deployments.values()) {
|
||||
if (['pending', 'in_progress'].includes(deployment.status)) {
|
||||
active.push(deployment);
|
||||
}
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
// Получить статистику
|
||||
getStats() {
|
||||
const stats = {
|
||||
total: this.deployments.size,
|
||||
pending: 0,
|
||||
inProgress: 0,
|
||||
completed: 0,
|
||||
failed: 0
|
||||
};
|
||||
|
||||
for (const deployment of this.deployments.values()) {
|
||||
switch (deployment.status) {
|
||||
case 'pending':
|
||||
stats.pending++;
|
||||
break;
|
||||
case 'in_progress':
|
||||
stats.inProgress++;
|
||||
break;
|
||||
case 'completed':
|
||||
stats.completed++;
|
||||
break;
|
||||
case 'failed':
|
||||
stats.failed++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton экземпляр
|
||||
const deploymentTracker = new DeploymentTracker();
|
||||
|
||||
module.exports = deploymentTracker;
|
||||
Reference in New Issue
Block a user