ваше сообщение коммита
This commit is contained in:
@@ -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;
|
||||
107
backend/utils/apiKeyManager.js
Normal file
107
backend/utils/apiKeyManager.js
Normal 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;
|
||||
153
backend/utils/constructorArgsGenerator.js
Normal file
153
backend/utils/constructorArgsGenerator.js
Normal 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
|
||||
};
|
||||
226
backend/utils/deploymentUtils.js
Normal file
226
backend/utils/deploymentUtils.js
Normal 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
|
||||
};
|
||||
353
backend/utils/nonceManager.js
Normal file
353
backend/utils/nonceManager.js
Normal 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
|
||||
};
|
||||
281
backend/utils/operationDecoder.js
Normal file
281
backend/utils/operationDecoder.js
Normal 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
|
||||
};
|
||||
250
backend/utils/rpcConnectionManager.js
Normal file
250
backend/utils/rpcConnectionManager.js
Normal 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;
|
||||
Reference in New Issue
Block a user