251 lines
9.3 KiB
JavaScript
251 lines
9.3 KiB
JavaScript
/**
|
||
* Менеджер 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;
|