ваше сообщение коммита

This commit is contained in:
2025-09-30 00:23:37 +03:00
parent ca718e3178
commit 4b03951b31
77 changed files with 17161 additions and 7255 deletions

View File

@@ -1,286 +0,0 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const { ethers } = require('ethers');
/**
* Менеджер nonce для синхронизации транзакций в мультичейн-деплое
* Обеспечивает правильную последовательность транзакций без конфликтов
*/
class NonceManager {
constructor() {
this.nonceCache = new Map(); // Кэш nonce для каждого кошелька
this.pendingTransactions = new Map(); // Ожидающие транзакции
this.locks = new Map(); // Блокировки для предотвращения конкурентного доступа
}
/**
* Получить актуальный nonce для кошелька в сети
* @param {string} rpcUrl - URL RPC провайдера
* @param {string} walletAddress - Адрес кошелька
* @param {boolean} usePending - Использовать pending транзакции
* @returns {Promise<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

@@ -0,0 +1,107 @@
/**
* Централизованный менеджер API ключей
* Унифицирует работу с API ключами Etherscan
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
*/
const logger = require('./logger');
class ApiKeyManager {
/**
* Получает API ключ Etherscan из различных источников
* @param {Object} params - Параметры деплоя
* @param {Object} reqBody - Тело запроса (опционально)
* @returns {string|null} - API ключ или null
*/
static getEtherscanApiKey(params = {}, reqBody = {}) {
// Приоритет источников:
// 1. Из параметров деплоя (БД)
// 2. Из тела запроса (фронтенд)
// 3. Из переменных окружения
// 4. Из секретов
let apiKey = null;
// 1. Из параметров деплоя (БД) - приоритет 1
if (params.etherscan_api_key) {
apiKey = params.etherscan_api_key;
logger.info('[API_KEY] ✅ Ключ получен из параметров деплоя (БД)');
}
// 2. Из тела запроса (фронтенд) - приоритет 2
else if (reqBody.etherscanApiKey) {
apiKey = reqBody.etherscanApiKey;
logger.info('[API_KEY] ✅ Ключ получен из тела запроса (фронтенд)');
}
// 3. Из переменных окружения - приоритет 3
else if (process.env.ETHERSCAN_API_KEY) {
apiKey = process.env.ETHERSCAN_API_KEY;
logger.info('[API_KEY] ✅ Ключ получен из переменных окружения');
}
// 4. Из секретов - приоритет 4
else if (process.env.ETHERSCAN_V2_API_KEY) {
apiKey = process.env.ETHERSCAN_V2_API_KEY;
logger.info('[API_KEY] ✅ Ключ получен из секретов');
}
if (apiKey) {
logger.info(`[API_KEY] 🔑 API ключ найден: ${apiKey.substring(0, 8)}...`);
return apiKey;
} else {
logger.warn('[API_KEY] ⚠️ API ключ Etherscan не найден');
return null;
}
}
/**
* Устанавливает API ключ в переменные окружения
* @param {string} apiKey - API ключ
*/
static setEtherscanApiKey(apiKey) {
if (apiKey) {
process.env.ETHERSCAN_API_KEY = apiKey;
logger.info(`[API_KEY] 🔧 API ключ установлен в переменные окружения: ${apiKey.substring(0, 8)}...`);
}
}
/**
* Проверяет наличие API ключа
* @param {string} apiKey - API ключ для проверки
* @returns {boolean} - true если ключ валидный
*/
static validateApiKey(apiKey) {
if (!apiKey || typeof apiKey !== 'string') {
logger.warn('[API_KEY] ❌ API ключ не валидный: пустой или не строка');
return false;
}
if (apiKey.length < 10) {
logger.warn('[API_KEY] ❌ API ключ слишком короткий');
return false;
}
logger.info('[API_KEY] ✅ API ключ валидный');
return true;
}
/**
* Получает и устанавливает API ключ (универсальный метод)
* @param {Object} params - Параметры деплоя
* @param {Object} reqBody - Тело запроса (опционально)
* @returns {string|null} - API ключ или null
*/
static getAndSetEtherscanApiKey(params = {}, reqBody = {}) {
const apiKey = this.getEtherscanApiKey(params, reqBody);
if (apiKey && this.validateApiKey(apiKey)) {
this.setEtherscanApiKey(apiKey);
return apiKey;
}
return null;
}
}
module.exports = ApiKeyManager;

View File

@@ -0,0 +1,153 @@
/**
* Централизованный генератор параметров конструктора для DLE контракта
* Обеспечивает одинаковые параметры для деплоя и верификации
*/
/**
* Генерирует параметры конструктора для DLE контракта
* @param {Object} params - Параметры деплоя из базы данных
* @param {number} chainId - ID сети для деплоя (опционально)
* @returns {Object} Объект с параметрами конструктора
*/
function generateDLEConstructorArgs(params, chainId = null) {
// Валидация обязательных параметров
if (!params) {
throw new Error('Параметры деплоя не переданы');
}
// Базовые параметры DLE
const dleConfig = {
name: params.name || '',
symbol: params.symbol || '',
location: params.location || '',
coordinates: params.coordinates || '',
jurisdiction: params.jurisdiction || 0,
okvedCodes: params.okvedCodes || [],
kpp: params.kpp ? BigInt(params.kpp) : 0n,
quorumPercentage: params.quorumPercentage || 50,
initialPartners: params.initialPartners || [],
// Умножаем initialAmounts на 1e18 для конвертации в wei
initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount) * BigInt(1e18)),
supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id))
};
// Определяем initializer
const initializer = params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000";
return {
dleConfig,
initializer
};
}
/**
* Генерирует параметры конструктора для верификации (с преобразованием в строки)
* @param {Object} params - Параметры деплоя из базы данных
* @param {number} chainId - ID сети для верификации (опционально)
* @returns {Array} Массив параметров конструктора для верификации
*/
function generateVerificationArgs(params, chainId = null) {
const { dleConfig, initializer } = generateDLEConstructorArgs(params, chainId);
// Для верификации нужно преобразовать BigInt в строки
const verificationConfig = {
...dleConfig,
initialAmounts: dleConfig.initialAmounts.map(amount => amount.toString()),
supportedChainIds: dleConfig.supportedChainIds.map(id => id.toString())
};
return [
verificationConfig,
initializer
];
}
/**
* Генерирует параметры конструктора для деплоя (с BigInt)
* @param {Object} params - Параметры деплоя из базы данных
* @param {number} chainId - ID сети для деплоя (опционально)
* @returns {Object} Объект с параметрами конструктора для деплоя
*/
function generateDeploymentArgs(params, chainId = null) {
const { dleConfig, initializer } = generateDLEConstructorArgs(params, chainId);
return {
dleConfig,
initializer
};
}
/**
* Валидирует параметры конструктора
* @param {Object} params - Параметры деплоя
* @returns {Object} Результат валидации
*/
function validateConstructorArgs(params) {
const errors = [];
const warnings = [];
// Проверяем обязательные поля
if (!params.name) errors.push('name не указан');
if (!params.symbol) errors.push('symbol не указан');
if (!params.location) errors.push('location не указан');
if (!params.coordinates) errors.push('coordinates не указаны');
if (!params.jurisdiction) errors.push('jurisdiction не указан');
if (!params.okvedCodes || !Array.isArray(params.okvedCodes)) errors.push('okvedCodes не указан или не является массивом');
if (!params.initialPartners || !Array.isArray(params.initialPartners)) errors.push('initialPartners не указан или не является массивом');
if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) errors.push('initialAmounts не указан или не является массивом');
if (!params.supportedChainIds || !Array.isArray(params.supportedChainIds)) errors.push('supportedChainIds не указан или не является массивом');
// Проверяем соответствие массивов
if (params.initialPartners && params.initialAmounts &&
params.initialPartners.length !== params.initialAmounts.length) {
errors.push('Количество initialPartners не соответствует количеству initialAmounts');
}
// Проверяем значения
if (params.quorumPercentage && (params.quorumPercentage < 1 || params.quorumPercentage > 100)) {
warnings.push('quorumPercentage должен быть от 1 до 100');
}
if (params.initialAmounts) {
const negativeAmounts = params.initialAmounts.filter(amount => amount < 0);
if (negativeAmounts.length > 0) {
errors.push('initialAmounts содержит отрицательные значения');
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Логирует параметры конструктора для отладки
* @param {Object} params - Параметры деплоя
* @param {string} context - Контекст (deployment/verification)
*/
function logConstructorArgs(params, context = 'unknown') {
console.log(`📊 [${context.toUpperCase()}] Параметры конструктора:`);
console.log(` name: "${params.name}"`);
console.log(` symbol: "${params.symbol}"`);
console.log(` location: "${params.location}"`);
console.log(` coordinates: "${params.coordinates}"`);
console.log(` jurisdiction: ${params.jurisdiction}`);
console.log(` okvedCodes: [${params.okvedCodes.join(', ')}]`);
console.log(` kpp: ${params.kpp}`);
console.log(` quorumPercentage: ${params.quorumPercentage}`);
console.log(` initialPartners: [${params.initialPartners.join(', ')}]`);
console.log(` initialAmounts: [${params.initialAmounts.join(', ')}]`);
console.log(` supportedChainIds: [${params.supportedChainIds.join(', ')}]`);
console.log(` governanceChainId: 1 (Ethereum)`);
console.log(` initializer: ${params.initializer}`);
}
module.exports = {
generateDLEConstructorArgs,
generateVerificationArgs,
generateDeploymentArgs,
validateConstructorArgs,
logConstructorArgs
};

View File

@@ -0,0 +1,226 @@
/**
* Общие утилиты для деплоя контрактов
* Устраняет дублирование кода между скриптами деплоя
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
*/
const { ethers } = require('ethers');
const logger = require('./logger');
const RPCConnectionManager = require('./rpcConnectionManager');
const { nonceManager } = require('./nonceManager');
/**
* Подбирает безопасные gas/fee для разных сетей (включая L2)
* @param {Object} provider - Провайдер ethers
* @param {Object} options - Опции для настройки
* @returns {Promise<Object>} - Объект с настройками газа
*/
async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) {
try {
const fee = await provider.getFeeData();
const overrides = {};
const minPriority = await ethers.parseUnits(minPriorityGwei.toString(), 'gwei');
const minFee = await ethers.parseUnits(minFeeGwei.toString(), 'gwei');
if (fee.maxFeePerGas) {
overrides.maxFeePerGas = fee.maxFeePerGas < minFee ? minFee : fee.maxFeePerGas;
overrides.maxPriorityFeePerGas = (fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n)
? fee.maxPriorityFeePerGas
: minPriority;
} else if (fee.gasPrice) {
overrides.gasPrice = fee.gasPrice < minFee ? minFee : fee.gasPrice;
}
return overrides;
} catch (error) {
logger.error('Ошибка при получении fee overrides:', error);
throw error;
}
}
/**
* Создает провайдер и кошелек для деплоя
* @param {string} rpcUrl - URL RPC
* @param {string} privateKey - Приватный ключ
* @returns {Object} - Объект с провайдером и кошельком
*/
function createProviderAndWallet(rpcUrl, privateKey) {
try {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const wallet = new ethers.Wallet(privateKey, provider);
return { provider, wallet };
} catch (error) {
logger.error('Ошибка при создании провайдера и кошелька:', error);
throw error;
}
}
/**
* Выравнивает nonce до целевого значения
* @param {Object} wallet - Кошелек ethers
* @param {Object} provider - Провайдер ethers
* @param {number} targetNonce - Целевой nonce
* @param {Object} options - Опции для настройки
* @returns {Promise<number>} - Текущий nonce после выравнивания
*/
async function alignNonce(wallet, provider, targetNonce, options = {}) {
try {
// Используем nonceManager для получения актуального nonce
const network = await provider.getNetwork();
const chainId = Number(network.chainId);
const rpcUrl = provider._getConnection?.()?.url || 'unknown';
let current = await nonceManager.getNonceFast(wallet.address, rpcUrl, chainId);
if (current > targetNonce) {
throw new Error(`Current nonce ${current} > target nonce ${targetNonce}`);
}
if (current < targetNonce) {
logger.info(`Выравнивание nonce: ${current} -> ${targetNonce} (${targetNonce - current} транзакций)`);
const { burnAddress = '0x000000000000000000000000000000000000dEaD' } = options;
for (let i = current; i < targetNonce; i++) {
const overrides = await getFeeOverrides(provider);
const gasLimit = 21000n;
try {
const txFill = await wallet.sendTransaction({
to: burnAddress,
value: 0,
gasLimit,
...overrides
});
logger.info(`Filler tx sent, hash=${txFill.hash}, nonce=${i}`);
await txFill.wait();
logger.info(`Filler tx confirmed, hash=${txFill.hash}, nonce=${i}`);
// Обновляем nonce в кэше
nonceManager.reserveNonce(wallet.address, chainId, i);
current = i + 1;
} catch (error) {
logger.error(`Filler tx failed for nonce=${i}:`, error);
throw error;
}
}
logger.info(`Nonce alignment completed, current nonce=${current}`);
} else {
logger.info(`Nonce already aligned at ${current}`);
}
return current;
} catch (error) {
logger.error('Ошибка при выравнивании nonce:', error);
throw error;
}
}
/**
* Получает информацию о сети
* @param {Object} provider - Провайдер ethers
* @returns {Promise<Object>} - Информация о сети
*/
async function getNetworkInfo(provider) {
try {
const network = await provider.getNetwork();
return {
chainId: Number(network.chainId),
name: network.name
};
} catch (error) {
logger.error('Ошибка при получении информации о сети:', error);
throw error;
}
}
/**
* Проверяет баланс кошелька
* @param {Object} provider - Провайдер ethers
* @param {string} address - Адрес кошелька
* @returns {Promise<string>} - Баланс в ETH
*/
async function getBalance(provider, address) {
try {
const balance = await provider.getBalance(address);
return ethers.formatEther(balance);
} catch (error) {
logger.error('Ошибка при получении баланса:', error);
throw error;
}
}
/**
* Создает RPC соединение с retry логикой
* @param {string} rpcUrl - URL RPC
* @param {string} privateKey - Приватный ключ
* @param {Object} options - Опции соединения
* @returns {Promise<Object>} - {provider, wallet, network}
*/
async function createRPCConnection(rpcUrl, privateKey, options = {}) {
const rpcManager = new RPCConnectionManager();
return await rpcManager.createConnection(rpcUrl, privateKey, options);
}
/**
* Создает множественные RPC соединения с обработкой ошибок
* @param {Array} rpcUrls - Массив RPC URL
* @param {string} privateKey - Приватный ключ
* @param {Object} options - Опции соединения
* @returns {Promise<Array>} - Массив успешных соединений
*/
async function createMultipleRPCConnections(rpcUrls, privateKey, options = {}) {
const rpcManager = new RPCConnectionManager();
return await rpcManager.createMultipleConnections(rpcUrls, privateKey, options);
}
/**
* Выполняет транзакцию с retry логикой
* @param {Object} wallet - Кошелек
* @param {Object} txData - Данные транзакции
* @param {Object} options - Опции
* @returns {Promise<Object>} - Результат транзакции
*/
async function sendTransactionWithRetry(wallet, txData, options = {}) {
const rpcManager = new RPCConnectionManager();
return await rpcManager.sendTransactionWithRetry(wallet, txData, options);
}
/**
* Получает nonce с retry логикой
* @param {Object} provider - Провайдер
* @param {string} address - Адрес
* @param {Object} options - Опции
* @returns {Promise<number>} - Nonce
*/
async function getNonceWithRetry(provider, address, options = {}) {
// Используем быстрый метод по умолчанию
if (options.fast !== false) {
try {
const network = await provider.getNetwork();
const chainId = Number(network.chainId);
const rpcUrl = provider._getConnection?.()?.url || 'unknown';
return await nonceManager.getNonceFast(address, rpcUrl, chainId);
} catch (error) {
console.warn(`[deploymentUtils] Быстрый nonce failed, используем retry: ${error.message}`);
}
}
// Fallback на retry метод
return await nonceManager.getNonceWithRetry(provider, address, options);
}
module.exports = {
getFeeOverrides,
createProviderAndWallet,
alignNonce,
getNetworkInfo,
getBalance,
createRPCConnection,
createMultipleRPCConnections,
sendTransactionWithRetry,
getNonceWithRetry
};

View File

@@ -0,0 +1,353 @@
/**
* Менеджер nonce для управления транзакциями в разных сетях
* Решает проблему "nonce too low" при деплое в нескольких сетях
*/
const { ethers } = require('ethers');
class NonceManager {
constructor() {
this.nonceCache = new Map(); // Кэш nonce для каждого адреса и сети
this.pendingTransactions = new Map(); // Отслеживание pending транзакций
}
/**
* Получить актуальный nonce для адреса в сети с таймаутом и retry логикой
* @param {string} address - Адрес кошелька
* @param {string} rpcUrl - RPC URL сети
* @param {number} chainId - ID сети
* @param {Object} options - Опции (timeout, maxRetries)
* @returns {Promise<number>} - Актуальный nonce
*/
async getNonce(address, rpcUrl, chainId, options = {}) {
const { timeout = 10000, maxRetries = 3 } = options; // Увеличиваем таймаут и попытки
const cacheKey = `${address}-${chainId}`;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const provider = new ethers.JsonRpcProvider(rpcUrl);
// Получаем nonce из сети с таймаутом
const networkNonce = await Promise.race([
provider.getTransactionCount(address, 'pending'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Nonce timeout')), timeout)
)
]);
// ВАЖНО: Не используем кэш для критических операций деплоя
// Всегда получаем актуальный nonce из сети
this.nonceCache.set(cacheKey, networkNonce);
console.log(`[NonceManager] ${address}:${chainId} nonce=${networkNonce} (попытка ${attempt})`);
return networkNonce;
} catch (error) {
console.error(`[NonceManager] Ошибка ${address}:${chainId} (${attempt}):`, error.message);
if (attempt === maxRetries) {
// В случае критической ошибки, сбрасываем кэш и пробуем еще раз
this.nonceCache.delete(cacheKey);
throw new Error(`Не удалось получить nonce после ${maxRetries} попыток: ${error.message}`);
}
// Увеличиваем задержку между попытками
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
}
}
}
/**
* Зарезервировать nonce для транзакции
* @param {string} address - Адрес кошелька
* @param {number} chainId - ID сети
* @param {number} nonce - Nonce для резервирования
*/
reserveNonce(address, chainId, nonce) {
const cacheKey = `${address}-${chainId}`;
const currentNonce = this.nonceCache.get(cacheKey) || 0;
if (nonce >= currentNonce) {
this.nonceCache.set(cacheKey, nonce + 1);
console.log(`[NonceManager] Зарезервирован nonce ${nonce} для ${address} в сети ${chainId}`);
} else {
console.warn(`[NonceManager] Попытка использовать nonce ${nonce} меньше текущего ${currentNonce} для ${address} в сети ${chainId}`);
}
}
/**
* Отметить транзакцию как pending
* @param {string} address - Адрес кошелька
* @param {number} chainId - ID сети
* @param {number} nonce - Nonce транзакции
* @param {string} txHash - Хэш транзакции
*/
markTransactionPending(address, chainId, nonce, txHash) {
const cacheKey = `${address}-${chainId}`;
const pendingTxs = this.pendingTransactions.get(cacheKey) || [];
pendingTxs.push({
nonce,
txHash,
timestamp: Date.now()
});
this.pendingTransactions.set(cacheKey, pendingTxs);
console.log(`[NonceManager] Отмечена pending транзакция ${txHash} с nonce ${nonce} для ${address} в сети ${chainId}`);
}
/**
* Отметить транзакцию как подтвержденную
* @param {string} address - Адрес кошелька
* @param {number} chainId - ID сети
* @param {string} txHash - Хэш транзакции
*/
markTransactionConfirmed(address, chainId, txHash) {
const cacheKey = `${address}-${chainId}`;
const pendingTxs = this.pendingTransactions.get(cacheKey) || [];
const txIndex = pendingTxs.findIndex(tx => tx.txHash === txHash);
if (txIndex !== -1) {
const tx = pendingTxs[txIndex];
pendingTxs.splice(txIndex, 1);
console.log(`[NonceManager] Транзакция ${txHash} подтверждена для ${address} в сети ${chainId}`);
}
}
/**
* Очистить старые pending транзакции
* @param {string} address - Адрес кошелька
* @param {number} chainId - ID сети
* @param {number} maxAge - Максимальный возраст в миллисекундах (по умолчанию 5 минут)
*/
clearOldPendingTransactions(address, chainId, maxAge = 5 * 60 * 1000) {
const cacheKey = `${address}-${chainId}`;
const pendingTxs = this.pendingTransactions.get(cacheKey) || [];
const now = Date.now();
const validTxs = pendingTxs.filter(tx => (now - tx.timestamp) < maxAge);
if (validTxs.length !== pendingTxs.length) {
this.pendingTransactions.set(cacheKey, validTxs);
console.log(`[NonceManager] Очищено ${pendingTxs.length - validTxs.length} старых pending транзакций для ${address} в сети ${chainId}`);
}
}
/**
* Получить информацию о pending транзакциях
* @param {string} address - Адрес кошелька
* @param {number} chainId - ID сети
* @returns {Array} - Массив pending транзакций
*/
getPendingTransactions(address, chainId) {
const cacheKey = `${address}-${chainId}`;
return this.pendingTransactions.get(cacheKey) || [];
}
/**
* Сбросить кэш nonce для адреса и сети
* @param {string} address - Адрес кошелька
* @param {number} chainId - ID сети
*/
resetNonce(address, chainId) {
const cacheKey = `${address}-${chainId}`;
this.nonceCache.delete(cacheKey);
this.pendingTransactions.delete(cacheKey);
console.log(`[NonceManager] Сброшен кэш nonce для ${address} в сети ${chainId}`);
}
/**
* Получить статистику по nonce
* @returns {Object} - Статистика
*/
getStats() {
return {
nonceCache: Object.fromEntries(this.nonceCache),
pendingTransactions: Object.fromEntries(this.pendingTransactions)
};
}
/**
* Быстрое получение nonce без retry (для критичных по времени операций)
* @param {string} address - Адрес кошелька
* @param {string} rpcUrl - RPC URL сети
* @param {number} chainId - ID сети
* @returns {Promise<number>} - Nonce
*/
async getNonceFast(address, rpcUrl, chainId) {
const cacheKey = `${address}-${chainId}`;
const cachedNonce = this.nonceCache.get(cacheKey);
if (cachedNonce !== undefined) {
console.log(`[NonceManager] Быстрый nonce из кэша: ${cachedNonce} для ${address}:${chainId}`);
return cachedNonce;
}
// Получаем RPC URLs из базы данных с fallback
const rpcUrls = await this.getRpcUrlsFromDatabase(chainId, rpcUrl);
for (const currentRpc of rpcUrls) {
try {
console.log(`[NonceManager] Пробуем RPC: ${currentRpc}`);
const provider = new ethers.JsonRpcProvider(currentRpc);
const networkNonce = await Promise.race([
provider.getTransactionCount(address, 'pending'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Fast nonce timeout')), 3000)
)
]);
this.nonceCache.set(cacheKey, networkNonce);
console.log(`[NonceManager] ✅ Nonce получен: ${networkNonce} для ${address}:${chainId} с RPC: ${currentRpc}`);
return networkNonce;
} catch (error) {
console.warn(`[NonceManager] RPC failed: ${currentRpc} - ${error.message}`);
continue;
}
}
// Если все RPC недоступны, возвращаем 0
console.warn(`[NonceManager] Все RPC недоступны для ${address}:${chainId}, возвращаем 0`);
this.nonceCache.set(cacheKey, 0);
return 0;
}
/**
* Получить RPC URLs из базы данных с fallback
* @param {number} chainId - ID сети
* @param {string} primaryRpcUrl - Основной RPC URL (опциональный)
* @returns {Promise<Array>} - Массив RPC URL
*/
async getRpcUrlsFromDatabase(chainId, primaryRpcUrl = null) {
const rpcUrls = [];
// Добавляем основной RPC URL если указан
if (primaryRpcUrl) {
rpcUrls.push(primaryRpcUrl);
}
try {
// Получаем RPC из deploy_params (как в deploy-multichain.js)
const DeployParamsService = require('../services/deployParamsService');
const deployParamsService = new DeployParamsService();
// Получаем последние параметры деплоя
const latestParams = await deployParamsService.getLatestDeployParams(1);
if (latestParams.length > 0) {
const params = latestParams[0];
const supportedChainIds = params.supported_chain_ids || [];
const rpcUrlsFromParams = params.rpc_urls || [];
// Находим RPC для нужного chainId
const chainIndex = supportedChainIds.indexOf(chainId);
if (chainIndex !== -1 && rpcUrlsFromParams[chainIndex]) {
const deployRpcUrl = rpcUrlsFromParams[chainIndex];
if (!rpcUrls.includes(deployRpcUrl)) {
rpcUrls.push(deployRpcUrl);
console.log(`[NonceManager] ✅ RPC из deploy_params для chainId ${chainId}: ${deployRpcUrl}`);
}
}
}
await deployParamsService.close();
} catch (error) {
console.warn(`[NonceManager] deploy_params недоступны для chainId ${chainId}, используем fallback: ${error.message}`);
}
// Всегда добавляем fallback RPC для надежности
const fallbackRPCs = this.getFallbackRPCs(chainId);
for (const fallbackRpc of fallbackRPCs) {
if (!rpcUrls.includes(fallbackRpc)) {
rpcUrls.push(fallbackRpc);
}
}
console.log(`[NonceManager] RPC URLs для chainId ${chainId}:`, rpcUrls);
return rpcUrls;
}
/**
* Получить список fallback RPC для сети
* @param {number} chainId - ID сети
* @returns {Array} - Массив RPC URL
*/
getFallbackRPCs(chainId) {
const fallbackRPCs = {
1: [ // Mainnet
'https://eth.llamarpc.com',
'https://rpc.ankr.com/eth',
'https://ethereum.publicnode.com'
],
11155111: [ // Sepolia
'https://rpc.sepolia.org',
'https://sepolia.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'
],
17000: [ // Holesky
'https://ethereum-holesky.publicnode.com',
'https://holesky.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161'
],
421614: [ // Arbitrum Sepolia
'https://sepolia-rollup.arbitrum.io/rpc'
],
84532: [ // Base Sepolia
'https://sepolia.base.org'
]
};
return fallbackRPCs[chainId] || [];
}
/**
* Интеграция с существующими системами - замена для rpcConnectionManager
* @param {Object} provider - Провайдер ethers
* @param {string} address - Адрес кошелька
* @param {Object} options - Опции
* @returns {Promise<number>} - Nonce
*/
async getNonceWithRetry(provider, address, options = {}) {
// Извлекаем chainId из провайдера
const network = await provider.getNetwork();
const chainId = Number(network.chainId);
// Получаем RPC URL из провайдера (если возможно)
const rpcUrl = provider._getConnection?.()?.url || 'unknown';
return await this.getNonce(address, rpcUrl, chainId, options);
}
/**
* Принудительно обновляет nonce из сети (для обработки race conditions)
* @param {string} address - Адрес кошелька
* @param {string} rpcUrl - RPC URL сети
* @param {number} chainId - ID сети
* @returns {Promise<number>} - Актуальный nonce
*/
async forceRefreshNonce(address, rpcUrl, chainId) {
const cacheKey = `${address}-${chainId}`;
try {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const networkNonce = await provider.getTransactionCount(address, 'pending');
// Принудительно обновляем кэш
this.nonceCache.set(cacheKey, networkNonce);
console.log(`[NonceManager] Force refreshed nonce for ${address}:${chainId} = ${networkNonce}`);
return networkNonce;
} catch (error) {
console.error(`[NonceManager] Force refresh failed for ${address}:${chainId}:`, error.message);
throw error;
}
}
}
// Создаем глобальный экземпляр
const nonceManager = new NonceManager();
module.exports = {
NonceManager,
nonceManager
};

View File

@@ -0,0 +1,281 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const { ethers } = require('ethers');
/**
* Декодирует операцию из формата abi.encodeWithSelector
* @param {string} operation - Закодированная операция (hex string)
* @returns {Object} - Декодированная операция
*/
function decodeOperation(operation) {
try {
if (!operation || operation.length < 4) {
return {
type: 'unknown',
selector: null,
data: null,
decoded: null,
error: 'Invalid operation format'
};
}
// Извлекаем селектор (первые 4 байта)
const selector = operation.slice(0, 10); // 0x + 4 байта
const data = operation.slice(10); // Остальные данные
// Определяем тип операции по селектору
const operationType = getOperationType(selector);
if (operationType === 'unknown') {
return {
type: 'unknown',
selector: selector,
data: data,
decoded: null,
error: 'Unknown operation selector'
};
}
// Декодируем данные в зависимости от типа операции
let decoded = null;
try {
decoded = decodeOperationData(operationType, data);
} catch (decodeError) {
return {
type: operationType,
selector: selector,
data: data,
decoded: null,
error: `Failed to decode ${operationType}: ${decodeError.message}`
};
}
return {
type: operationType,
selector: selector,
data: data,
decoded: decoded,
error: null
};
} catch (error) {
return {
type: 'error',
selector: null,
data: null,
decoded: null,
error: error.message
};
}
}
/**
* Определяет тип операции по селектору
* @param {string} selector - Селектор функции (0x + 4 байта)
* @returns {string} - Тип операции
*/
function getOperationType(selector) {
const selectors = {
'0x12345678': '_addModule', // Пример селектора
'0x87654321': '_removeModule', // Пример селектора
'0xabcdef12': '_addSupportedChain', // Пример селектора
'0x21fedcba': '_removeSupportedChain', // Пример селектора
'0x1234abcd': '_transferTokens', // Пример селектора
'0xabcd1234': '_updateVotingDurations', // Пример селектора
'0x5678efgh': '_setLogoURI', // Пример селектора
'0xefgh5678': '_updateQuorumPercentage', // Пример селектора
'0x9abc1234': '_updateDLEInfo', // Пример селектора
'0x12349abc': 'offchainAction' // Пример селектора
};
// Вычисляем реальные селекторы
const realSelectors = {
[ethers.id('_addModule(bytes32,address)').slice(0, 10)]: '_addModule',
[ethers.id('_removeModule(bytes32)').slice(0, 10)]: '_removeModule',
[ethers.id('_addSupportedChain(uint256)').slice(0, 10)]: '_addSupportedChain',
[ethers.id('_removeSupportedChain(uint256)').slice(0, 10)]: '_removeSupportedChain',
[ethers.id('_transferTokens(address,uint256)').slice(0, 10)]: '_transferTokens',
[ethers.id('_updateVotingDurations(uint256,uint256)').slice(0, 10)]: '_updateVotingDurations',
[ethers.id('_setLogoURI(string)').slice(0, 10)]: '_setLogoURI',
[ethers.id('_updateQuorumPercentage(uint256)').slice(0, 10)]: '_updateQuorumPercentage',
[ethers.id('_updateDLEInfo(string,string,string,string,uint256,string[],uint256)').slice(0, 10)]: '_updateDLEInfo',
[ethers.id('offchainAction(bytes32,string,bytes32)').slice(0, 10)]: 'offchainAction'
};
return realSelectors[selector] || 'unknown';
}
/**
* Декодирует данные операции в зависимости от типа
* @param {string} operationType - Тип операции
* @param {string} data - Закодированные данные
* @returns {Object} - Декодированные данные
*/
function decodeOperationData(operationType, data) {
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
switch (operationType) {
case '_addModule':
const [moduleId, moduleAddress] = abiCoder.decode(['bytes32', 'address'], '0x' + data);
return {
moduleId: moduleId,
moduleAddress: moduleAddress
};
case '_removeModule':
const [moduleIdToRemove] = abiCoder.decode(['bytes32'], '0x' + data);
return {
moduleId: moduleIdToRemove
};
case '_addSupportedChain':
const [chainIdToAdd] = abiCoder.decode(['uint256'], '0x' + data);
return {
chainId: Number(chainIdToAdd)
};
case '_removeSupportedChain':
const [chainIdToRemove] = abiCoder.decode(['uint256'], '0x' + data);
return {
chainId: Number(chainIdToRemove)
};
case '_transferTokens':
const [recipient, amount] = abiCoder.decode(['address', 'uint256'], '0x' + data);
return {
recipient: recipient,
amount: amount.toString(),
amountFormatted: ethers.formatEther(amount)
};
case '_updateVotingDurations':
const [minDuration, maxDuration] = abiCoder.decode(['uint256', 'uint256'], '0x' + data);
return {
minDuration: Number(minDuration),
maxDuration: Number(maxDuration)
};
case '_setLogoURI':
const [logoURI] = abiCoder.decode(['string'], '0x' + data);
return {
logoURI: logoURI
};
case '_updateQuorumPercentage':
const [quorumPercentage] = abiCoder.decode(['uint256'], '0x' + data);
return {
quorumPercentage: Number(quorumPercentage)
};
case '_updateDLEInfo':
const [name, symbol, location, coordinates, jurisdiction, okvedCodes, kpp] = abiCoder.decode(
['string', 'string', 'string', 'string', 'uint256', 'string[]', 'uint256'],
'0x' + data
);
return {
name: name,
symbol: symbol,
location: location,
coordinates: coordinates,
jurisdiction: Number(jurisdiction),
okvedCodes: okvedCodes,
kpp: Number(kpp)
};
case 'offchainAction':
const [actionId, kind, payloadHash] = abiCoder.decode(['bytes32', 'string', 'bytes32'], '0x' + data);
return {
actionId: actionId,
kind: kind,
payloadHash: payloadHash
};
default:
throw new Error(`Unknown operation type: ${operationType}`);
}
}
/**
* Форматирует декодированную операцию для отображения
* @param {Object} decodedOperation - Декодированная операция
* @returns {string} - Отформатированное описание
*/
function formatOperation(decodedOperation) {
if (decodedOperation.error) {
return `Ошибка: ${decodedOperation.error}`;
}
const { type, decoded } = decodedOperation;
switch (type) {
case '_addModule':
return `Добавить модуль: ${decoded.moduleId} (${decoded.moduleAddress})`;
case '_removeModule':
return `Удалить модуль: ${decoded.moduleId}`;
case '_addSupportedChain':
return `Добавить поддерживаемую сеть: ${decoded.chainId}`;
case '_removeSupportedChain':
return `Удалить поддерживаемую сеть: ${decoded.chainId}`;
case '_transferTokens':
return `Перевести токены: ${decoded.amountFormatted} DLE на адрес ${decoded.recipient}`;
case '_updateVotingDurations':
return `Обновить длительность голосования: ${decoded.minDuration}-${decoded.maxDuration} секунд`;
case '_setLogoURI':
return `Обновить логотип: ${decoded.logoURI}`;
case '_updateQuorumPercentage':
return `Обновить процент кворума: ${decoded.quorumPercentage}%`;
case '_updateDLEInfo':
return `Обновить информацию DLE: ${decoded.name} (${decoded.symbol})`;
case 'offchainAction':
return `Оффчейн действие: ${decoded.kind} (${decoded.actionId})`;
default:
return `Неизвестная операция: ${type}`;
}
}
/**
* Получает название сети по ID
* @param {number} chainId - ID сети
* @returns {string} - Название сети
*/
function getChainName(chainId) {
const chainNames = {
1: 'Ethereum Mainnet',
11155111: 'Sepolia',
17000: 'Holesky',
421614: 'Arbitrum Sepolia',
84532: 'Base Sepolia',
137: 'Polygon',
80001: 'Polygon Mumbai',
56: 'BSC',
97: 'BSC Testnet'
};
return chainNames[chainId] || `Chain ${chainId}`;
}
module.exports = {
decodeOperation,
formatOperation,
getChainName
};

View File

@@ -0,0 +1,250 @@
/**
* Менеджер RPC соединений с retry логикой и обработкой ошибок
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
*/
const { ethers } = require('ethers');
const logger = require('./logger');
class RPCConnectionManager {
constructor() {
this.connections = new Map(); // Кэш соединений
this.retryConfig = {
maxRetries: 3,
baseDelay: 1000, // 1 секунда
maxDelay: 10000, // 10 секунд
timeout: 30000 // 30 секунд
};
}
/**
* Создает RPC соединение с retry логикой
* @param {string} rpcUrl - URL RPC
* @param {string} privateKey - Приватный ключ
* @param {Object} options - Опции соединения
* @returns {Promise<Object>} - {provider, wallet, network}
*/
async createConnection(rpcUrl, privateKey, options = {}) {
const config = { ...this.retryConfig, ...options };
const connectionKey = `${rpcUrl}_${privateKey}`;
// Проверяем кэш
if (this.connections.has(connectionKey)) {
const cached = this.connections.get(connectionKey);
if (Date.now() - cached.timestamp < 60000) { // 1 минута кэш
logger.info(`[RPC_MANAGER] Используем кэшированное соединение: ${rpcUrl}`);
return cached.connection;
}
}
logger.info(`[RPC_MANAGER] Создаем новое RPC соединение: ${rpcUrl}`);
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
polling: false,
staticNetwork: false
});
// Проверяем соединение с timeout
const network = await Promise.race([
provider.getNetwork(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('RPC timeout')), config.timeout)
)
]);
const wallet = new ethers.Wallet(privateKey, provider);
const connection = { provider, wallet, network };
// Кэшируем соединение
this.connections.set(connectionKey, {
connection,
timestamp: Date.now()
});
logger.info(`[RPC_MANAGER] ✅ RPC соединение установлено: ${rpcUrl} (chainId: ${network.chainId})`);
return connection;
} catch (error) {
logger.error(`[RPC_MANAGER] ❌ Попытка ${attempt}/${config.maxRetries} failed: ${error.message}`);
if (attempt === config.maxRetries) {
throw new Error(`RPC соединение не удалось установить после ${config.maxRetries} попыток: ${error.message}`);
}
// Экспоненциальная задержка
const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
logger.info(`[RPC_MANAGER] Ожидание ${delay}ms перед повторной попыткой...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/**
* Создает множественные RPC соединения с обработкой ошибок
* @param {Array} rpcUrls - Массив RPC URL
* @param {string} privateKey - Приватный ключ
* @param {Object} options - Опции соединения
* @returns {Promise<Array>} - Массив успешных соединений
*/
async createMultipleConnections(rpcUrls, privateKey, options = {}) {
logger.info(`[RPC_MANAGER] Создаем ${rpcUrls.length} RPC соединений...`);
const connectionPromises = rpcUrls.map(async (rpcUrl, index) => {
try {
const connection = await this.createConnection(rpcUrl, privateKey, options);
return { index, rpcUrl, ...connection, success: true };
} catch (error) {
logger.error(`[RPC_MANAGER] ❌ Соединение ${index + 1} failed: ${rpcUrl} - ${error.message}`);
return { index, rpcUrl, error: error.message, success: false };
}
});
const results = await Promise.all(connectionPromises);
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
logger.info(`[RPC_MANAGER] ✅ Успешных соединений: ${successful.length}/${rpcUrls.length}`);
if (failed.length > 0) {
logger.warn(`[RPC_MANAGER] ⚠️ Неудачных соединений: ${failed.length}`);
failed.forEach(f => logger.warn(`[RPC_MANAGER] - ${f.rpcUrl}: ${f.error}`));
}
if (successful.length === 0) {
throw new Error('Не удалось установить ни одного RPC соединения');
}
return successful;
}
/**
* Выполняет транзакцию с retry логикой
* @param {Object} wallet - Кошелек
* @param {Object} txData - Данные транзакции
* @param {Object} options - Опции
* @returns {Promise<Object>} - Результат транзакции
*/
async sendTransactionWithRetry(wallet, txData, options = {}) {
const config = { ...this.retryConfig, ...options };
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
logger.info(`[RPC_MANAGER] Отправка транзакции (попытка ${attempt}/${config.maxRetries})`);
const tx = await wallet.sendTransaction({
...txData,
timeout: config.timeout
});
logger.info(`[RPC_MANAGER] ✅ Транзакция отправлена: ${tx.hash}`);
// Ждем подтверждения с timeout
const receipt = await Promise.race([
tx.wait(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Transaction timeout')), config.timeout)
)
]);
logger.info(`[RPC_MANAGER] ✅ Транзакция подтверждена: ${tx.hash}`);
return { tx, receipt, success: true };
} catch (error) {
logger.error(`[RPC_MANAGER] ❌ Транзакция failed (попытка ${attempt}): ${error.message}`);
if (attempt === config.maxRetries) {
throw new Error(`Транзакция не удалась после ${config.maxRetries} попыток: ${error.message}`);
}
// Проверяем, стоит ли повторять
if (this.shouldRetry(error)) {
const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
logger.info(`[RPC_MANAGER] Ожидание ${delay}ms перед повторной попыткой...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
}
/**
* Определяет, стоит ли повторять операцию
* @param {Error} error - Ошибка
* @returns {boolean} - Стоит ли повторять
*/
shouldRetry(error) {
const retryableErrors = [
'NETWORK_ERROR',
'TIMEOUT',
'ECONNRESET',
'ENOTFOUND',
'ETIMEDOUT',
'RPC timeout',
'Transaction timeout'
];
const errorMessage = error.message.toLowerCase();
return retryableErrors.some(retryableError =>
errorMessage.includes(retryableError.toLowerCase())
);
}
/**
* Получает nonce с retry логикой
* @param {Object} provider - Провайдер
* @param {string} address - Адрес
* @param {Object} options - Опции
* @returns {Promise<number>} - Nonce
*/
async getNonceWithRetry(provider, address, options = {}) {
const config = { ...this.retryConfig, ...options };
for (let attempt = 1; attempt <= config.maxRetries; attempt++) {
try {
const nonce = await Promise.race([
provider.getTransactionCount(address, 'pending'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Nonce timeout')), config.timeout)
)
]);
logger.info(`[RPC_MANAGER] ✅ Nonce получен: ${nonce} (попытка ${attempt})`);
return nonce;
} catch (error) {
logger.error(`[RPC_MANAGER] ❌ Nonce failed (попытка ${attempt}): ${error.message}`);
if (attempt === config.maxRetries) {
throw new Error(`Не удалось получить nonce после ${config.maxRetries} попыток: ${error.message}`);
}
const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/**
* Очищает кэш соединений
*/
clearCache() {
this.connections.clear();
logger.info('[RPC_MANAGER] Кэш соединений очищен');
}
/**
* Получает статистику соединений
* @returns {Object} - Статистика
*/
getStats() {
return {
cachedConnections: this.connections.size,
retryConfig: this.retryConfig
};
}
}
module.exports = RPCConnectionManager;