/** * Copyright (c) 2024-2026 Тарабанов Александр Викторович * 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/VC-HB3-Accelerator */ /** * Менеджер nonce для управления транзакциями в разных сетях * Решает проблему "nonce too low" при деплое в нескольких сетях */ const { ethers } = require('ethers'); const { getRpcUrls } = require('./networkLoader'); 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} - Актуальный nonce */ async getNonce(address, rpcUrl, chainId, options = {}) { const { timeout = 15000, maxRetries = 5 } = options; // Увеличиваем таймаут и попытки // КРИТИЧЕСКАЯ ПРОВЕРКА: логируем входящие параметры console.log(`[NonceManager] getNonce вызван с параметрами: address=${address}, rpcUrl=${rpcUrl}, chainId=${chainId}`); // КРИТИЧЕСКАЯ ПРОВЕРКА: если rpcUrl содержит 127.0.0.1:8545, это ошибка! if (rpcUrl && rpcUrl.includes('127.0.0.1:8545')) { console.error(`[NonceManager] ❌ КРИТИЧЕСКАЯ ОШИБКА: Получен неправильный rpcUrl: ${rpcUrl} для chainId ${chainId}`); throw new Error(`Получен неправильный rpcUrl: ${rpcUrl} для chainId ${chainId}`); } const cacheKey = `${address}-${chainId}`; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`[NonceManager] Попытка ${attempt}: создаем JsonRpcProvider с rpcUrl: ${rpcUrl}`); const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { polling: false, // Отключаем polling для более быстрого получения nonce staticNetwork: true }); // Получаем 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) }; } // getNonceFast функция удалена - используем getNonce() вместо этого /** * Получить RPC URLs из базы данных с fallback * @param {number} chainId - ID сети * @param {string} primaryRpcUrl - Основной RPC URL (опциональный) * @returns {Promise} - Массив RPC URL */ async getRpcUrlsFromDatabase(chainId, primaryRpcUrl = null) { const rpcUrls = []; // Добавляем основной RPC URL если указан if (primaryRpcUrl) { rpcUrls.push(primaryRpcUrl); } try { // Используем networkLoader для получения RPC URLs const { getRpcUrls } = require('./networkLoader'); const rpcUrlsFromLoader = await getRpcUrls(); // Получаем RPC для конкретного chainId const chainRpcUrl = rpcUrlsFromLoader[chainId] || rpcUrlsFromLoader[chainId.toString()]; if (chainRpcUrl && !rpcUrls.includes(chainRpcUrl)) { rpcUrls.push(chainRpcUrl); console.log(`[NonceManager] ✅ RPC из networkLoader для chainId ${chainId}: ${chainRpcUrl}`); } } catch (error) { console.warn(`[NonceManager] networkLoader недоступен для chainId ${chainId}, используем fallback: ${error.message}`); } // Всегда добавляем fallback RPC для надежности ИЗ БАЗЫ ДАННЫХ const fallbackRPCs = await 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 из базы данных */ async getFallbackRPCs(chainId) { try { // Получаем ВСЕ RPC провайдеры для данной сети из базы данных const rpcService = require('../services/rpcProviderService'); const providers = await rpcService.getAllRpcProviders(); // Фильтруем по chain_id const networkProviders = providers.filter(p => p.chain_id === chainId); if (networkProviders.length === 0) { console.warn(`[NonceManager] Нет RPC провайдеров в базе данных для chain_id: ${chainId}`); return []; } // Возвращаем только RPC URL из базы данных return networkProviders.map(p => p.rpc_url).filter(url => url); } catch (error) { console.error(`[NonceManager] Ошибка получения RPC из базы данных для chain_id ${chainId}:`, error); return []; } } /** * Интеграция с существующими системами - замена для rpcConnectionManager * @param {Object} provider - Провайдер ethers * @param {string} address - Адрес кошелька * @param {Object} options - Опции * @returns {Promise} - 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} - Актуальный nonce */ async forceRefreshNonce(address, rpcUrl, chainId) { const cacheKey = `${address}-${chainId}`; try { const provider = new ethers.JsonRpcProvider(await rpcService.getRpcUrlByChainId(chainId)); 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; } } /** * Выровнять nonce до целевого значения с помощью filler транзакций * @param {string} address - Адрес кошелька * @param {string} rpcUrl - RPC URL сети * @param {number} chainId - ID сети * @param {number} targetNonce - Целевой nonce * @param {Object} wallet - Кошелек для отправки транзакций * @param {Object} options - Опции (gasLimit, maxRetries) * @returns {Promise} - Финальный nonce */ async alignNonceToTarget(address, rpcUrl, chainId, targetNonce, wallet, options = {}) { const { gasLimit = 21000, maxRetries = 5 } = options; const burnAddress = "0x000000000000000000000000000000000000dEaD"; // Получаем текущий nonce let current = await this.getNonce(address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); console.log(`[NonceManager] Aligning nonce ${current} -> ${targetNonce} for ${address}:${chainId}`); if (current >= targetNonce) { console.log(`[NonceManager] Nonce already aligned: ${current} >= ${targetNonce}`); return current; } // Используем RPCConnectionManager для retry логики const RPCConnectionManager = require('./rpcConnectionManager'); const rpcManager = new RPCConnectionManager(); // Выравниваем nonce с помощью filler транзакций while (current < targetNonce) { console.log(`[NonceManager] Sending filler tx nonce=${current} for ${address}:${chainId}`); try { const txData = { to: burnAddress, value: 0n, nonce: current, gasLimit, }; const result = await rpcManager.sendTransactionWithRetry(wallet, txData, { maxRetries }); console.log(`[NonceManager] Filler tx sent: ${result.tx.hash} for nonce=${current}`); current++; } catch (e) { const errorMsg = e?.message || e; console.warn(`[NonceManager] Filler tx failed: ${errorMsg}`); // Обрабатываем ошибки nonce if (String(errorMsg).toLowerCase().includes('nonce too low')) { this.resetNonce(address, chainId); const newNonce = await this.getNonce(address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); console.log(`[NonceManager] Nonce changed from ${current} to ${newNonce}`); current = newNonce; if (current >= targetNonce) { console.log(`[NonceManager] Nonce alignment completed: ${current} >= ${targetNonce}`); return current; } continue; } throw new Error(`Failed to send filler tx for nonce=${current}: ${errorMsg}`); } } console.log(`[NonceManager] Nonce alignment completed: ${current} for ${address}:${chainId}`); return current; } } // Создаем глобальный экземпляр const nonceManager = new NonceManager(); module.exports = { NonceManager, nonceManager };