diff --git a/SETUP_ACCESS_LEVELS.md b/SETUP_ACCESS_LEVELS.md new file mode 100644 index 0000000..507f873 --- /dev/null +++ b/SETUP_ACCESS_LEVELS.md @@ -0,0 +1,105 @@ +# Настройка расширенной системы прав доступа + +## Описание изменений + +Добавлена расширенная система прав доступа с настраиваемыми порогами: + +- **Read-Only (1+ токен)** - только просмотр данных +- **Editor (2+ токен)** - просмотр + редактирование + удаление +- **User (0 токенов)** - базовые права без изменений + +## Новые поля в форме добавления токенов + +На странице `/settings/security` в форме добавления токенов добавлены два новых поля: + +1. **Минимум токенов для Read-Only доступа** (по умолчанию: 1) +2. **Минимум токенов для Editor доступа** (по умолчанию: 2) + +## Применение изменений + +### 1. Обновление базы данных + +Выполните SQL скрипт для добавления новых полей: + +```bash +# Подключитесь к базе данных PostgreSQL +psql -h localhost -U your_username -d your_database + +# Выполните миграцию +\i backend/scripts/add_access_thresholds.sql +``` + +### 2. Перезапуск сервера + +```bash +# В папке backend +yarn restart +# или +docker-compose restart backend +``` + +### 3. Перезапуск frontend + +```bash +# В папке frontend +yarn dev +``` + +## Как использовать + +### Для администраторов: + +1. Перейдите на страницу `/settings/security` +2. В разделе "Токены аутентификации" нажмите "Подробнее" +3. При добавлении нового токена заполните: + - Название токена + - Адрес смарт-контракта + - Сеть блокчейна + - Минимальный баланс + - **Минимум токенов для Read-Only доступа** + - **Минимум токенов для Editor доступа** + +### Для пользователей: + +- Система автоматически определяет уровень доступа на основе количества токенов +- Отображается текущий уровень доступа с визуальными индикаторами +- UI автоматически адаптируется под уровень доступа + +## Примеры настроек + +### Стандартные настройки: +- Read-Only: 1 токен +- Editor: 2 токена + +### Строгие настройки: +- Read-Only: 2 токена +- Editor: 5 токенов + +### Либеральные настройки: +- Read-Only: 1 токен +- Editor: 1 токен (все пользователи с токенами могут редактировать) + +## Технические детали + +### Backend изменения: +- `auth-service.js` - добавлена функция `getUserAccessLevel()` +- `authTokenService.js` - поддержка новых полей +- `tokenBalanceService.js` - возврат порогов доступа +- `routes/settings.js` - API endpoint для новых полей +- `routes/auth.js` - новый endpoint `/access-level/:address` + +### Frontend изменения: +- `useAuth.js` - поддержка уровней доступа +- `usePermissions.js` - новый composable для проверки прав +- `AuthTokensSettings.vue` - форма с новыми полями +- `SecuritySettingsView.vue` - использование новых прав доступа + +### Новые поля в БД: +- `auth_tokens.readonly_threshold` - порог для Read-Only +- `auth_tokens.editor_threshold` - порог для Editor + +## Обратная совместимость + +- Все существующие токены получают значения по умолчанию (1 и 2) +- Старые проверки `isAdmin` продолжают работать +- Система автоматически мигрирует существующие данные diff --git a/backend/routes/auth.js b/backend/routes/auth.js index d42bda1..8acec30 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -946,4 +946,25 @@ router.get('/check-tokens/:address', async (req, res) => { } }); +// Маршрут для получения уровня доступа пользователя +router.get('/access-level/:address', async (req, res) => { + try { + const { address } = req.params; + + // Получаем уровень доступа пользователя + const accessLevel = await authService.getUserAccessLevel(address); + + res.json({ + success: true, + data: accessLevel, + }); + } catch (error) { + logger.error('Error getting user access level:', error); + res.status(500).json({ + success: false, + error: 'Internal server error', + }); + } +}); + module.exports = router; diff --git a/backend/routes/ollama.js b/backend/routes/ollama.js index e32f053..57c10a8 100644 --- a/backend/routes/ollama.js +++ b/backend/routes/ollama.js @@ -21,20 +21,28 @@ const { requireAuth } = require('../middleware/auth'); // Проверка статуса подключения к Ollama router.get('/status', requireAuth, async (req, res) => { try { - // Проверяем, что контейнер Ollama запущен - const { stdout } = await execAsync('docker ps --filter "name=dapp-ollama" --format "{{.Names}}"'); - const isContainerRunning = stdout.trim() === 'dapp-ollama'; + const axios = require('axios'); + const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434'; - if (!isContainerRunning) { - return res.json({ connected: false, error: 'Ollama container not running' }); - } - - // Проверяем API Ollama + // Проверяем API Ollama через HTTP запрос try { - const { stdout: apiResponse } = await execAsync('docker exec dapp-ollama ollama list'); - return res.json({ connected: true, message: 'Ollama is running' }); + const response = await axios.get(`${ollamaUrl}/api/tags`, { + timeout: 5000 // 5 секунд таймаут + }); + + const models = response.data.models || []; + return res.json({ + connected: true, + message: 'Ollama is running', + models: models.length, + availableModels: models.map(m => m.name) + }); } catch (apiError) { - return res.json({ connected: false, error: 'Ollama API not responding' }); + logger.error('Ollama API error:', apiError.message); + return res.json({ + connected: false, + error: `Ollama API not responding: ${apiError.message}` + }); } } catch (error) { logger.error('Error checking Ollama status:', error); @@ -45,21 +53,19 @@ router.get('/status', requireAuth, async (req, res) => { // Получение списка установленных моделей router.get('/models', requireAuth, async (req, res) => { try { - const { stdout } = await execAsync('docker exec dapp-ollama ollama list'); - const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок + const axios = require('axios'); + const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434'; - const models = lines.map(line => { - const parts = line.trim().split(/\s+/); - if (parts.length >= 4) { - return { - name: parts[0], - id: parts[1], - size: parseInt(parts[2]) || 0, - modified: parts.slice(3).join(' ') - }; - } - return null; - }).filter(model => model !== null); + const response = await axios.get(`${ollamaUrl}/api/tags`, { + timeout: 5000 + }); + + const models = (response.data.models || []).map(model => ({ + name: model.name, + id: model.model || model.name, + size: model.size || 0, + modified: model.modified_at || 'Unknown' + })); res.json({ models }); } catch (error) { diff --git a/backend/routes/settings.js b/backend/routes/settings.js index 41ad19a..62f4bff 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -177,7 +177,7 @@ router.get('/auth-tokens', async (req, res, next) => { } const tokensResult = await db.getQuery()( - 'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens', + 'SELECT id, min_balance, readonly_threshold, editor_threshold, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens', [encryptionKey] ); const authTokens = tokensResult.rows; @@ -218,11 +218,11 @@ router.post('/auth-tokens', requireAdmin, async (req, res, next) => { // Добавление/обновление одного токена router.post('/auth-token', requireAdmin, async (req, res, next) => { try { - const { name, address, network, minBalance } = req.body; + const { name, address, network, minBalance, readonlyThreshold, editorThreshold } = req.body; if (!name || !address || !network) { return res.status(400).json({ success: false, error: 'name, address и network обязательны' }); } - await authTokenService.upsertAuthToken({ name, address, network, minBalance }); + await authTokenService.upsertAuthToken({ name, address, network, minBalance, readonlyThreshold, editorThreshold }); // Отправляем WebSocket уведомление о добавлении токена try { diff --git a/backend/services/aiProviderSettingsService.js b/backend/services/aiProviderSettingsService.js index 852ce09..63101d3 100644 --- a/backend/services/aiProviderSettingsService.js +++ b/backend/services/aiProviderSettingsService.js @@ -35,8 +35,8 @@ async function upsertProviderSettings({ provider, api_key, base_url, selected_mo const existing = await encryptedDb.getData(TABLE, { provider: provider }, 1); if (existing.length > 0) { - // Обновляем существующую запись - return await encryptedDb.saveData(TABLE, data, { provider: provider }); + // Обновляем существующую запись по ID + return await encryptedDb.saveData(TABLE, data, { id: existing[0].id }); } else { // Создаем новую запись return await encryptedDb.saveData(TABLE, data); @@ -120,29 +120,51 @@ async function getAllLLMModels() { for (const provider of providers) { if (provider.selected_model) { - allModels.push({ - id: provider.selected_model, - provider: provider.provider - }); + // Фильтруем embedding модели - они не должны быть в списке LLM + const modelName = provider.selected_model.toLowerCase(); + const isEmbeddingModel = modelName.includes('embed') || + modelName.includes('embedding') || + modelName.includes('bge') || + modelName.includes('nomic') || + modelName.includes('text-embedding') || + modelName.includes('mxbai') || + modelName.includes('sentence') || + modelName.includes('ada-002') || + modelName.includes('text-embedding-ada') || + modelName.includes('text-embedding-3'); + + if (!isEmbeddingModel) { + allModels.push({ + id: provider.selected_model, + provider: provider.provider + }); + } } } - // Для Ollama проверяем реально установленные модели + // Для Ollama проверяем реально установленные модели через HTTP API try { - const { exec } = require('child_process'); - const util = require('util'); - const execAsync = util.promisify(exec); + const axios = require('axios'); + const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434'; - // Проверяем, какие модели установлены в Ollama - const { stdout } = await execAsync('docker exec dapp-ollama ollama list'); - const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок + const response = await axios.get(`${ollamaUrl}/api/tags`, { + timeout: 5000 + }); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - if (parts.length >= 2) { - const modelName = parts[0]; + const models = response.data.models || []; + for (const model of models) { + // Фильтруем embedding модели из Ollama + const modelName = model.name.toLowerCase(); + const isEmbeddingModel = modelName.includes('embed') || + modelName.includes('embedding') || + modelName.includes('bge') || + modelName.includes('nomic') || + modelName.includes('mxbai') || + modelName.includes('sentence'); + + if (!isEmbeddingModel) { allModels.push({ - id: modelName, + id: model.name, provider: 'ollama' }); } @@ -189,27 +211,23 @@ async function getAllEmbeddingModels() { } } - // Для Ollama проверяем реально установленные embedding модели + // Для Ollama проверяем реально установленные embedding модели через HTTP API try { - const { exec } = require('child_process'); - const util = require('util'); - const execAsync = util.promisify(exec); + const axios = require('axios'); + const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434'; - // Проверяем, какие embedding модели установлены в Ollama - const { stdout } = await execAsync('docker exec dapp-ollama ollama list'); - const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок + const response = await axios.get(`${ollamaUrl}/api/tags`, { + timeout: 5000 + }); - for (const line of lines) { - const parts = line.trim().split(/\s+/); - if (parts.length >= 2) { - const modelName = parts[0]; - // Проверяем, что это embedding модель - if (modelName.includes('embed') || modelName.includes('bge') || modelName.includes('nomic')) { - allModels.push({ - id: modelName, - provider: 'ollama' - }); - } + const models = response.data.models || []; + for (const model of models) { + // Проверяем, что это embedding модель + if (model.name.includes('embed') || model.name.includes('bge') || model.name.includes('nomic')) { + allModels.push({ + id: model.name, + provider: 'ollama' + }); } } } catch (ollamaError) { diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index ffe07c5..b5dda4b 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -466,6 +466,191 @@ class AuthService { } } + /** + * Определяет уровень доступа пользователя на основе количества токенов + * @param {string} address - Адрес кошелька + * @returns {Promise<{level: string, tokenCount: number, hasAccess: boolean}>} + */ + async getUserAccessLevel(address) { + if (!address) { + return { level: 'user', tokenCount: 0, hasAccess: false }; + } + + logger.info(`Checking access level for address: ${address}`); + + try { + // Получаем токены из базы данных напрямую (как в checkAdminRole) + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + } + } catch (keyError) { + console.error('Error reading encryption key:', keyError); + } + + // Получаем токены из базы с расшифровкой + const tokensResult = await db.getQuery()( + 'SELECT id, min_balance, readonly_threshold, editor_threshold, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens', + [encryptionKey] + ); + const tokens = tokensResult.rows; + + // Получаем RPC провайдеры + const rpcProvidersResult = await db.getQuery()( + 'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers', + [encryptionKey] + ); + const rpcProviders = rpcProvidersResult.rows; + + const rpcMap = {}; + for (const rpc of rpcProviders) { + rpcMap[rpc.network_id] = rpc.rpc_url; + } + + // Получаем балансы токенов из блокчейна + const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)']; + const tokenBalances = []; + + for (const token of tokens) { + const rpcUrl = rpcMap[token.network]; + if (!rpcUrl) continue; + + try { + const provider = new ethers.JsonRpcProvider(rpcUrl); + const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider); + + // Получаем баланс с таймаутом + const balancePromise = tokenContract.balanceOf(address); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 5000) // Увеличиваем таймаут до 5 секунд + ); + + const rawBalance = await Promise.race([balancePromise, timeoutPromise]); + const balance = ethers.formatUnits(rawBalance, 18); + + tokenBalances.push({ + network: token.network, + tokenAddress: token.address, + tokenName: token.name, + symbol: '', + balance, + minBalance: token.min_balance, + readonlyThreshold: token.readonly_threshold || 1, + editorThreshold: token.editor_threshold || 2, + }); + + logger.info(`[getUserAccessLevel] Token balance for ${token.name} (${token.address}): ${balance}`); + } catch (error) { + logger.error(`[getUserAccessLevel] Error getting balance for ${token.name} (${token.address}):`, error.message); + // Добавляем токен с нулевым балансом + tokenBalances.push({ + network: token.network, + tokenAddress: token.address, + tokenName: token.name, + symbol: '', + balance: '0', + minBalance: token.min_balance, + readonlyThreshold: token.readonly_threshold || 1, + editorThreshold: token.editor_threshold || 2, + }); + } + } + + if (!tokenBalances || !Array.isArray(tokenBalances)) { + logger.warn(`No token balances found for address: ${address}`); + return { level: 'user', tokenCount: 0, hasAccess: false }; + } + + // Подсчитываем сумму токенов с достаточным балансом + let validTokenCount = 0; + const validTokens = []; + + for (const token of tokenBalances) { + const balance = parseFloat(token.balance || '0'); + const minBalance = parseFloat(token.minBalance || '0'); + + if (balance >= minBalance) { + validTokenCount += balance; // Суммируем баланс токенов, а не количество сетей + validTokens.push({ + name: token.name, + network: token.network, + balance: balance, + minBalance: minBalance + }); + } + } + + logger.info(`Token validation for ${address}:`, { + totalTokens: tokenBalances.length, + validTokens: validTokenCount, + validTokenDetails: validTokens + }); + + // Определяем уровень доступа на основе настроек токенов + let accessLevel = 'user'; + let hasAccess = false; + + // Получаем настройки порогов из токенов (используем самые низкие требования для максимального доступа) + let readonlyThreshold = Infinity; + let editorThreshold = Infinity; + + if (tokenBalances.length > 0) { + // Находим самые низкие пороги среди всех токенов + for (const token of tokenBalances) { + const tokenReadonlyThreshold = token.readonlyThreshold || 1; + const tokenEditorThreshold = token.editorThreshold || 2; + + if (tokenReadonlyThreshold < readonlyThreshold) { + readonlyThreshold = tokenReadonlyThreshold; + } + if (tokenEditorThreshold < editorThreshold) { + editorThreshold = tokenEditorThreshold; + } + } + + // Если не нашли токены с порогами, используем дефолтные значения + if (readonlyThreshold === Infinity) readonlyThreshold = 1; + if (editorThreshold === Infinity) editorThreshold = 2; + + logger.info(`[AuthService] Определены пороги доступа: readonly=${readonlyThreshold}, editor=${editorThreshold} (из ${tokenBalances.length} токенов)`); + } else { + readonlyThreshold = 1; + editorThreshold = 2; + } + + if (validTokenCount >= editorThreshold) { + // Достаточно токенов для полных прав редактора + accessLevel = 'editor'; + hasAccess = true; + } else if (validTokenCount > 0) { + // Есть токены, но недостаточно для редактора - права только на чтение + accessLevel = 'readonly'; + hasAccess = true; + } else { + // Нет токенов - обычный пользователь + accessLevel = 'user'; + hasAccess = false; + } + + logger.info(`Access level determined for ${address}: ${accessLevel} (${validTokenCount} tokens)`); + + return { + level: accessLevel, + tokenCount: validTokenCount, + hasAccess: hasAccess, + validTokens: validTokens + }; + } catch (error) { + logger.error(`Error in getUserAccessLevel: ${error.message}`); + return { level: 'user', tokenCount: 0, hasAccess: false }; + } + } + // Добавляем псевдоним функции checkAdminRole для обратной совместимости async checkAdminTokens(address) { if (!address) return false; @@ -473,9 +658,11 @@ class AuthService { logger.info(`Checking admin tokens for address: ${address}`); try { - const isAdmin = await checkAdminRole(address); + // Используем новую функцию для определения уровня доступа + const accessLevel = await this.getUserAccessLevel(address); + const isAdmin = accessLevel.hasAccess; // Любой доступ выше 'user' считается админским - // Обновляем роль пользователя в базе данных, если есть админские токены + // Обновляем роль пользователя в базе данных if (isAdmin) { try { // Получаем ключ шифрования @@ -503,16 +690,17 @@ class AuthService { if (userResult.rows.length > 0) { const userId = userResult.rows[0].id; - // Обновляем роль пользователя - await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); - logger.info(`Updated user ${userId} role to admin based on token holdings`); + // Обновляем роль пользователя с учетом уровня доступа + const role = accessLevel.level; + await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, userId]); + logger.info(`Updated user ${userId} role to ${role} based on token holdings (${accessLevel.tokenCount} tokens)`); } } catch (error) { logger.error('Error updating user role:', error); // Продолжаем выполнение, даже если обновление роли не удалось } } else { - // Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin" + // Если пользователь не имеет доступа, сбрасываем роль на "user" try { // Получаем ключ шифрования const fs = require('fs'); @@ -536,10 +724,10 @@ class AuthService { [address.toLowerCase(), encryptionKey] ); - if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') { + if (userResult.rows.length > 0 && userResult.rows[0].role !== 'user') { const userId = userResult.rows[0].id; await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]); - logger.info(`Reset user ${userId} role from admin to user (no tokens found)`); + logger.info(`Reset user ${userId} role to user (no valid tokens found)`); } } catch (error) { logger.error('Error updating user role:', error); @@ -594,20 +782,18 @@ class AuthService { const address = user.address; const currentRole = user.role; - logger.info(`Rechecking admin status for user ${user.id} with address ${address}`); + logger.info(`Rechecking access level for user ${user.id} with address ${address}`); - // Проверяем баланс токенов - const isAdmin = await checkAdminRole(address); - - // Определяем новую роль - const newRole = isAdmin ? 'admin' : 'user'; + // Получаем новый уровень доступа + const accessLevel = await this.getUserAccessLevel(address); + const newRole = accessLevel.hasAccess ? accessLevel.level : 'user'; // Обновляем роль только если она изменилась if (currentRole !== newRole) { await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, user.id]); - logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address})`); + logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address}, tokens: ${accessLevel.tokenCount})`); } else { - logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address})`); + logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address}, tokens: ${accessLevel.tokenCount})`); } } catch (userError) { diff --git a/backend/services/authTokenService.js b/backend/services/authTokenService.js index 1688832..814b9ea 100644 --- a/backend/services/authTokenService.js +++ b/backend/services/authTokenService.js @@ -27,13 +27,25 @@ async function saveAllAuthTokens(authTokens) { name: token.name, address: token.address, network: token.network, - min_balance: token.minBalance == null ? 0 : Number(token.minBalance) + min_balance: token.minBalance == null ? 0 : Number(token.minBalance), + readonly_threshold: token.readonlyThreshold == null ? 1 : Number(token.readonlyThreshold), + editor_threshold: token.editorThreshold == null ? 2 : Number(token.editorThreshold) }); } } async function upsertAuthToken(token) { + console.log('[AuthTokenService] Получены данные токена:', token); + console.log('[AuthTokenService] token.readonlyThreshold:', token.readonlyThreshold, 'тип:', typeof token.readonlyThreshold); + console.log('[AuthTokenService] token.editorThreshold:', token.editorThreshold, 'тип:', typeof token.editorThreshold); + const minBalance = token.minBalance == null ? 0 : Number(token.minBalance); + const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold); + const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold); + + console.log('[AuthTokenService] Вычисленные значения:'); + console.log('[AuthTokenService] readonlyThreshold:', readonlyThreshold); + console.log('[AuthTokenService] editorThreshold:', editorThreshold); // Проверяем, существует ли токен const existingTokens = await encryptedDb.getData('auth_tokens', { @@ -45,7 +57,9 @@ async function upsertAuthToken(token) { // Обновляем существующий токен await encryptedDb.saveData('auth_tokens', { name: token.name, - min_balance: minBalance + min_balance: minBalance, + readonly_threshold: readonlyThreshold, + editor_threshold: editorThreshold }, { address: token.address, network: token.network @@ -56,7 +70,9 @@ async function upsertAuthToken(token) { name: token.name, address: token.address, network: token.network, - min_balance: minBalance + min_balance: minBalance, + readonly_threshold: readonlyThreshold, + editor_threshold: editorThreshold }); } } diff --git a/backend/services/encryptedDatabaseService.js b/backend/services/encryptedDatabaseService.js index 20ae3f6..92825c5 100644 --- a/backend/services/encryptedDatabaseService.js +++ b/backend/services/encryptedDatabaseService.js @@ -289,12 +289,23 @@ class EncryptedDataService { .map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`) .join(', '); const whereClause = Object.keys(whereConditions) - .map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`) + .map((key, index) => { + // Для WHERE условий используем зашифрованные имена колонок + const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`); + if (encryptedColumn) { + // Для зашифрованных колонок используем encrypt_text для сравнения + return `${quoteReservedWord(`${key}_encrypted`)} = encrypt_text($${paramIndex + index}, ${hasEncryptedFields ? '$1' : 'NULL'})`; + } else { + // Для незашифрованных колонок используем обычное сравнение + return `${quoteReservedWord(key)} = $${paramIndex + index}`; + } + }) .join(' AND '); const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`; const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)]; + const { rows } = await db.getQuery()(query, allParams); return rows[0]; } else { diff --git a/backend/services/tokenBalanceService.js b/backend/services/tokenBalanceService.js index cb6544e..9181437 100644 --- a/backend/services/tokenBalanceService.js +++ b/backend/services/tokenBalanceService.js @@ -37,7 +37,7 @@ async function getUserTokenBalances(address) { // Получаем токены и RPC с расшифровкой const tokensResult = await db.getQuery()( - 'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens', + 'SELECT id, min_balance, readonly_threshold, editor_threshold, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens', [encryptionKey] ); const tokens = tokensResult.rows; @@ -57,18 +57,42 @@ async function getUserTokenBalances(address) { for (const token of tokens) { const rpcUrl = rpcMap[token.network]; - if (!rpcUrl) continue; - const provider = new ethers.JsonRpcProvider(rpcUrl); + if (!rpcUrl) { + logger.warn(`[tokenBalanceService] RPC URL не найден для сети ${token.network}`); + continue; + } + + // Создаем провайдер с таймаутом + const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { + polling: false, + staticNetwork: true + }); + + // Устанавливаем таймаут для запросов + provider._getConnection().timeout = 10000; // 10 секунд + const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider); let balance = '0'; + try { - const rawBalance = await tokenContract.balanceOf(address); + logger.info(`[tokenBalanceService] Получение баланса для ${token.name} (${token.address}) в сети ${token.network} для адреса ${address}`); + + // Создаем промис с таймаутом + const balancePromise = tokenContract.balanceOf(address); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timeout')), 10000) + ); + + const rawBalance = await Promise.race([balancePromise, timeoutPromise]); balance = ethers.formatUnits(rawBalance, 18); + if (!balance || isNaN(Number(balance))) balance = '0'; + + logger.info(`[tokenBalanceService] Баланс получен для ${token.name}: ${balance}`); } catch (e) { logger.error( `[tokenBalanceService] Ошибка получения баланса для ${token.name} (${token.address}) в сети ${token.network}:`, - e + e.message || e ); balance = '0'; } @@ -79,6 +103,8 @@ async function getUserTokenBalances(address) { symbol: token.symbol || '', balance, minBalance: token.min_balance, + readonlyThreshold: token.readonly_threshold || 1, + editorThreshold: token.editor_threshold || 2, }); } diff --git a/backup_deploy_20250922_220227/deploy-multichain.js b/backup_deploy_20250922_220227/deploy-multichain.js deleted file mode 100644 index a032b8f..0000000 --- a/backup_deploy_20250922_220227/deploy-multichain.js +++ /dev/null @@ -1,617 +0,0 @@ -/* eslint-disable no-console */ -const hre = require('hardhat'); -const path = require('path'); -const fs = require('fs'); - -// Подбираем безопасные gas/fee для разных сетей (включая L2) -async function getFeeOverrides(provider, { minPriorityGwei = 1n, minFeeGwei = 20n } = {}) { - const fee = await provider.getFeeData(); - const overrides = {}; - const minPriority = (await (async () => hre.ethers.parseUnits(minPriorityGwei.toString(), 'gwei'))()); - const minFee = (await (async () => hre.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; -} - -async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit) { - const { ethers } = hre; - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(pk, provider); - const net = await provider.getNetwork(); - - // DEBUG: базовая информация по сети - try { - const calcInitHash = ethers.keccak256(dleInit); - const saltLen = ethers.getBytes(salt).length; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} rpc=${rpcUrl}`); - console.log(`[MULTI_DBG] wallet=${wallet.address} targetDLENonce=${targetDLENonce}`); - console.log(`[MULTI_DBG] saltLenBytes=${saltLen} salt=${salt}`); - console.log(`[MULTI_DBG] initCodeHash(provided)=${initCodeHash}`); - console.log(`[MULTI_DBG] initCodeHash(calculated)=${calcInitHash}`); - console.log(`[MULTI_DBG] dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`); - } catch (e) { - console.log('[MULTI_DBG] precheck error', e?.message || e); - } - - // 1) Выравнивание nonce до targetDLENonce нулевыми транзакциями (если нужно) - let current = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce=${current} target=${targetDLENonce}`); - - if (current > targetDLENonce) { - throw new Error(`Current nonce ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}`); - } - - if (current < targetDLENonce) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); - - // Используем burn address для более надежных транзакций - const burnAddress = "0x000000000000000000000000000000000000dEaD"; - - while (current < targetDLENonce) { - const overrides = await getFeeOverrides(provider); - let gasLimit = 21000; // минимальный gas для обычной транзакции - let sent = false; - let lastErr = null; - - for (let attempt = 0; attempt < 3 && !sent; attempt++) { - try { - const txReq = { - to: burnAddress, // отправляем на burn address вместо своего адреса - value: 0n, - nonce: current, - gasLimit, - ...overrides - }; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`); - const txFill = await wallet.sendTransaction(txReq); - await txFill.wait(); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed`); - sent = true; - } catch (e) { - lastErr = e; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${e?.message || e}`); - - if (String(e?.message || '').toLowerCase().includes('intrinsic gas too low') && attempt < 2) { - gasLimit = 50000; // увеличиваем gas limit - continue; - } - - if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) { - // Обновляем nonce и пробуем снова - current = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`); - continue; - } - - throw e; - } - } - - if (!sent) { - console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); - throw lastErr || new Error('filler tx failed'); - } - - current++; - } - - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); - } else { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); - } - - // 2) Деплой DLE напрямую на согласованном nonce - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLE directly with nonce=${targetDLENonce}`); - - const feeOverrides = await getFeeOverrides(provider); - let gasLimit; - - try { - // Оцениваем газ для деплоя DLE - const est = await wallet.estimateGas({ data: dleInit, ...feeOverrides }).catch(() => null); - - // Рассчитываем доступный gasLimit из баланса - const balance = await provider.getBalance(wallet.address, 'latest'); - const effPrice = feeOverrides.maxFeePerGas || feeOverrides.gasPrice || 0n; - const reserve = hre.ethers.parseEther('0.005'); - const maxByBalance = effPrice > 0n && balance > reserve ? (balance - reserve) / effPrice : 3_000_000n; - const fallbackGas = maxByBalance > 5_000_000n ? 5_000_000n : (maxByBalance < 2_500_000n ? 2_500_000n : maxByBalance); - gasLimit = est ? (est + est / 5n) : fallbackGas; - - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} estGas=${est?.toString?.()||'null'} effGasPrice=${effPrice?.toString?.()||'0'} maxByBalance=${maxByBalance.toString()} chosenGasLimit=${gasLimit.toString()}`); - } catch (_) { - gasLimit = 3_000_000n; - } - - // Вычисляем предсказанный адрес DLE - const predictedAddress = ethers.getCreateAddress({ - from: wallet.address, - nonce: targetDLENonce - }); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} predicted DLE address=${predictedAddress}`); - - // Проверяем, не развернут ли уже контракт - const existingCode = await provider.getCode(predictedAddress); - if (existingCode && existingCode !== '0x') { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE already exists at predictedAddress, skip deploy`); - return { address: predictedAddress, chainId: Number(net.chainId) }; - } - - // Деплоим DLE - let tx; - try { - tx = await wallet.sendTransaction({ - data: dleInit, - nonce: targetDLENonce, - gasLimit, - ...feeOverrides - }); - } catch (e) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy error(first): ${e?.message || e}`); - // Повторная попытка с обновленным nonce - const updatedNonce = await provider.getTransactionCount(wallet.address, 'pending'); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} retry deploy with nonce=${updatedNonce}`); - tx = await wallet.sendTransaction({ - data: dleInit, - nonce: updatedNonce, - gasLimit, - ...feeOverrides - }); - } - - const rc = await tx.wait(); - const deployedAddress = rc.contractAddress || predictedAddress; - - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE deployed at=${deployedAddress}`); - return { address: deployedAddress, chainId: Number(net.chainId) }; -} - -// Деплой модулей в одной сети -async function deployModulesInNetwork(rpcUrl, pk, dleAddress, params) { - const { ethers } = hre; - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(pk, provider); - const net = await provider.getNetwork(); - - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying modules...`); - - const modules = {}; - - // Получаем начальный nonce для всех модулей - let currentNonce = await wallet.getNonce(); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce for modules: ${currentNonce}`); - - // Функция для безопасного деплоя с правильным nonce - async function deployWithNonce(contractFactory, args, moduleName) { - try { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying ${moduleName} with nonce: ${currentNonce}`); - - // Проверяем, что nonce актуален - const actualNonce = await wallet.getNonce(); - if (actualNonce > currentNonce) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch, updating from ${currentNonce} to ${actualNonce}`); - currentNonce = actualNonce; - } - - const contract = await contractFactory.connect(wallet).deploy(...args); - await contract.waitForDeployment(); - const address = await contract.getAddress(); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployed at: ${address}`); - currentNonce++; - return address; - } catch (error) { - console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployment failed:`, error.message); - // Даже при ошибке увеличиваем nonce, чтобы не было конфликтов - currentNonce++; - return null; - } - } - - // Деплой TreasuryModule - const TreasuryModule = await hre.ethers.getContractFactory('TreasuryModule'); - modules.treasuryModule = await deployWithNonce( - TreasuryModule, - [dleAddress, Number(net.chainId), wallet.address], // _dleContract, _chainId, _emergencyAdmin - 'TreasuryModule' - ); - - // Деплой TimelockModule - const TimelockModule = await hre.ethers.getContractFactory('TimelockModule'); - modules.timelockModule = await deployWithNonce( - TimelockModule, - [dleAddress], // _dleContract - 'TimelockModule' - ); - - // Деплой DLEReader - const DLEReader = await hre.ethers.getContractFactory('DLEReader'); - modules.dleReader = await deployWithNonce( - DLEReader, - [dleAddress], // _dleContract - 'DLEReader' - ); - - // Инициализация модулей в DLE - try { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing modules in DLE with nonce: ${currentNonce}`); - - // Проверяем, что nonce актуален - const actualNonce = await wallet.getNonce(); - if (actualNonce > currentNonce) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before module init, updating from ${currentNonce} to ${actualNonce}`); - currentNonce = actualNonce; - } - - const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); - - // Проверяем, что все модули задеплоены - const treasuryAddress = modules.treasuryModule; - const timelockAddress = modules.timelockModule; - const readerAddress = modules.dleReader; - - if (treasuryAddress && timelockAddress && readerAddress) { - // Инициализация базовых модулей - await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`); - currentNonce++; - } else { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`); - } - } catch (error) { - console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message); - // Даже при ошибке увеличиваем nonce - currentNonce++; - } - - // Инициализация logoURI - try { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI with nonce: ${currentNonce}`); - - // Проверяем, что nonce актуален - const actualNonce = await wallet.getNonce(); - if (actualNonce > currentNonce) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before logoURI init, updating from ${currentNonce} to ${actualNonce}`); - currentNonce = actualNonce; - } - - // Используем логотип из параметров деплоя или fallback - const logoURL = params.logoURI || "https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE"; - const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); - await dleContract.initializeLogoURI(logoURL); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized: ${logoURL}`); - currentNonce++; - } catch (e) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${e.message}`); - // Fallback на базовый логотип - try { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} trying fallback logoURI with nonce: ${currentNonce}`); - const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); - await dleContract.initializeLogoURI("https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE"); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI initialized`); - currentNonce++; - } catch (fallbackError) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI also failed: ${fallbackError.message}`); - // Даже при ошибке увеличиваем nonce - currentNonce++; - } - } - - return modules; -} - -// Деплой модулей во всех сетях -async function deployModulesInAllNetworks(networks, pk, dleAddress, params) { - const moduleResults = []; - - for (let i = 0; i < networks.length; i++) { - const rpcUrl = networks[i]; - console.log(`[MULTI_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`); - - try { - const modules = await deployModulesInNetwork(rpcUrl, pk, dleAddress, params); - moduleResults.push(modules); - } catch (error) { - console.error(`[MULTI_DBG] Failed to deploy modules in network ${i + 1}:`, error.message); - moduleResults.push({ error: error.message }); - } - } - - return moduleResults; -} - -// Верификация контрактов в одной сети -async function verifyContractsInNetwork(rpcUrl, pk, dleAddress, modules, params) { - const { ethers } = hre; - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(pk, provider); - const net = await provider.getNetwork(); - - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting verification...`); - - const verification = {}; - - try { - // Верификация DLE - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying DLE...`); - await hre.run("verify:verify", { - address: dleAddress, - constructorArguments: [ - { - name: params.name || '', - symbol: params.symbol || '', - location: params.location || '', - coordinates: params.coordinates || '', - jurisdiction: params.jurisdiction || 0, - oktmo: params.oktmo || '', - okvedCodes: params.okvedCodes || [], - kpp: params.kpp ? BigInt(params.kpp) : 0n, - quorumPercentage: params.quorumPercentage || 51, - initialPartners: params.initialPartners || [], - initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount)), - supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id)) - }, - BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), - params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000" - ], - }); - verification.dle = 'success'; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification successful`); - } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification failed: ${error.message}`); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification error details:`, error); - verification.dle = 'failed'; - } - - // Верификация модулей - if (modules && !modules.error) { - try { - // Верификация TreasuryModule - if (modules.treasuryModule) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying TreasuryModule...`); - await hre.run("verify:verify", { - address: modules.treasuryModule, - constructorArguments: [ - dleAddress, // _dleContract - Number(net.chainId), // _chainId - wallet.address // _emergencyAdmin - ], - }); - verification.treasuryModule = 'success'; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule verification successful`); - } - } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule verification failed: ${error.message}`); - verification.treasuryModule = 'failed'; - } - - try { - // Верификация TimelockModule - if (modules.timelockModule) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying TimelockModule...`); - await hre.run("verify:verify", { - address: modules.timelockModule, - constructorArguments: [ - dleAddress // _dleContract - ], - }); - verification.timelockModule = 'success'; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule verification successful`); - } - } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule verification failed: ${error.message}`); - verification.timelockModule = 'failed'; - } - - try { - // Верификация DLEReader - if (modules.dleReader) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} verifying DLEReader...`); - await hre.run("verify:verify", { - address: modules.dleReader, - constructorArguments: [ - dleAddress // _dleContract - ], - }); - verification.dleReader = 'success'; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader verification successful`); - } - } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader verification failed: ${error.message}`); - verification.dleReader = 'failed'; - } - } - - return verification; -} - -// Верификация контрактов во всех сетях -async function verifyContractsInAllNetworks(networks, pk, dleAddress, moduleResults, params) { - const verificationResults = []; - - for (let i = 0; i < networks.length; i++) { - const rpcUrl = networks[i]; - console.log(`[MULTI_DBG] verifying contracts in network ${i + 1}/${networks.length}: ${rpcUrl}`); - - try { - const verification = await verifyContractsInNetwork(rpcUrl, pk, dleAddress, moduleResults[i], params); - verificationResults.push(verification); - } catch (error) { - console.error(`[MULTI_DBG] Failed to verify contracts in network ${i + 1}:`, error.message); - verificationResults.push({ error: error.message }); - } - } - - return verificationResults; -} - -async function main() { - const { ethers } = hre; - - // Загружаем параметры из файла - const paramsPath = path.join(__dirname, './current-params.json'); - if (!fs.existsSync(paramsPath)) { - throw new Error('Файл параметров не найден: ' + paramsPath); - } - - const params = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); - console.log('[MULTI_DBG] Загружены параметры:', { - name: params.name, - symbol: params.symbol, - supportedChainIds: params.supportedChainIds, - CREATE2_SALT: params.CREATE2_SALT - }); - - const pk = process.env.PRIVATE_KEY; - const salt = params.CREATE2_SALT; - const networks = params.rpcUrls || []; - - if (!pk) throw new Error('Env: PRIVATE_KEY'); - if (!salt) throw new Error('CREATE2_SALT not found in params'); - if (networks.length === 0) throw new Error('RPC URLs not found in params'); - - // Prepare init code once - const DLE = await hre.ethers.getContractFactory('DLE'); - const dleConfig = { - name: params.name || '', - symbol: params.symbol || '', - location: params.location || '', - coordinates: params.coordinates || '', - jurisdiction: params.jurisdiction || 0, - oktmo: params.oktmo || '', - okvedCodes: params.okvedCodes || [], - kpp: params.kpp ? BigInt(params.kpp) : 0n, - quorumPercentage: params.quorumPercentage || 51, - initialPartners: params.initialPartners || [], - initialAmounts: (params.initialAmounts || []).map(amount => BigInt(amount)), - supportedChainIds: (params.supportedChainIds || []).map(id => BigInt(id)) - }; - const deployTx = await DLE.getDeployTransaction(dleConfig, BigInt(params.currentChainId || params.supportedChainIds?.[0] || 1), params.initializer || params.initialPartners?.[0] || "0x0000000000000000000000000000000000000000"); - const dleInit = deployTx.data; - const initCodeHash = ethers.keccak256(dleInit); - - // DEBUG: глобальные значения - try { - const saltLen = ethers.getBytes(salt).length; - console.log(`[MULTI_DBG] GLOBAL saltLenBytes=${saltLen} salt=${salt}`); - console.log(`[MULTI_DBG] GLOBAL initCodeHash(calculated)=${initCodeHash}`); - console.log(`[MULTI_DBG] GLOBAL dleInit.lenBytes=${ethers.getBytes(dleInit).length} head16=${dleInit.slice(0, 34)}...`); - } catch (e) { - console.log('[MULTI_DBG] GLOBAL precheck error', e?.message || e); - } - - // Подготовим провайдеры и вычислим общий nonce для DLE - const providers = networks.map(u => new hre.ethers.JsonRpcProvider(u)); - const wallets = providers.map(p => new hre.ethers.Wallet(pk, p)); - const nonces = []; - for (let i = 0; i < providers.length; i++) { - const n = await providers[i].getTransactionCount(wallets[i].address, 'pending'); - nonces.push(n); - } - const targetDLENonce = Math.max(...nonces); - console.log(`[MULTI_DBG] nonces=${JSON.stringify(nonces)} targetDLENonce=${targetDLENonce}`); - - const results = []; - for (let i = 0; i < networks.length; i++) { - const rpcUrl = networks[i]; - console.log(`[MULTI_DBG] deploying to network ${i + 1}/${networks.length}: ${rpcUrl}`); - const r = await deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, dleInit); - results.push({ rpcUrl, ...r }); - } - - // Проверяем, что все адреса одинаковые - const addresses = results.map(r => r.address); - const uniqueAddresses = [...new Set(addresses)]; - - if (uniqueAddresses.length > 1) { - console.error('[MULTI_DBG] ERROR: DLE addresses are different across networks!'); - console.error('[MULTI_DBG] addresses:', uniqueAddresses); - throw new Error('Nonce alignment failed - addresses are different'); - } - - console.log('[MULTI_DBG] SUCCESS: All DLE addresses are identical:', uniqueAddresses[0]); - - // Деплой модулей во всех сетях - console.log('[MULTI_DBG] Starting module deployment...'); - const moduleResults = await deployModulesInAllNetworks(networks, pk, uniqueAddresses[0], params); - - // Верификация контрактов - console.log('[MULTI_DBG] Starting contract verification...'); - const verificationResults = await verifyContractsInAllNetworks(networks, pk, uniqueAddresses[0], moduleResults, params); - - // Объединяем результаты - const finalResults = results.map((result, index) => ({ - ...result, - modules: moduleResults[index] || {}, - verification: verificationResults[index] || {} - })); - - console.log('MULTICHAIN_DEPLOY_RESULT', JSON.stringify(finalResults)); - - // Сохраняем информацию о модулях в отдельный файл для каждого DLE - // Добавляем информацию о сетях (chainId, rpcUrl) - const modulesInfo = { - dleAddress: uniqueAddresses[0], - networks: networks.map((rpcUrl, index) => ({ - rpcUrl: rpcUrl, - chainId: null, // Будет заполнено ниже - networkName: null // Будет заполнено ниже - })), - modules: moduleResults, - verification: verificationResults, - deployTimestamp: new Date().toISOString() - }; - - // Получаем chainId для каждой сети - for (let i = 0; i < networks.length; i++) { - try { - const provider = new hre.ethers.JsonRpcProvider(networks[i]); - const network = await provider.getNetwork(); - modulesInfo.networks[i].chainId = Number(network.chainId); - - // Определяем название сети по chainId - const networkNames = { - 1: 'Ethereum Mainnet', - 5: 'Goerli', - 11155111: 'Sepolia', - 137: 'Polygon Mainnet', - 80001: 'Mumbai', - 56: 'BSC Mainnet', - 97: 'BSC Testnet', - 42161: 'Arbitrum One', - 421614: 'Arbitrum Sepolia', - 10: 'Optimism', - 11155420: 'Optimism Sepolia', - 8453: 'Base', - 84532: 'Base Sepolia' - }; - modulesInfo.networks[i].networkName = networkNames[Number(network.chainId)] || `Chain ID ${Number(network.chainId)}`; - - console.log(`[MULTI_DBG] Сеть ${i + 1}: chainId=${Number(network.chainId)}, name=${modulesInfo.networks[i].networkName}`); - } catch (error) { - console.error(`[MULTI_DBG] Ошибка получения chainId для сети ${i + 1}:`, error.message); - modulesInfo.networks[i].chainId = null; - modulesInfo.networks[i].networkName = `Сеть ${i + 1}`; - } - } - - // Создаем директорию temp если её нет - const tempDir = path.join(__dirname, '../temp'); - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }); - } - - const deployResultPath = path.join(tempDir, `modules-${uniqueAddresses[0].toLowerCase()}.json`); - fs.writeFileSync(deployResultPath, JSON.stringify(modulesInfo, null, 2)); - console.log(`[MULTI_DBG] Modules info saved to: ${deployResultPath}`); -} - -main().catch((e) => { console.error(e); process.exit(1); }); - - diff --git a/backup_deploy_20250922_220227/dleV2.js b/backup_deploy_20250922_220227/dleV2.js deleted file mode 100644 index 2bf0343..0000000 --- a/backup_deploy_20250922_220227/dleV2.js +++ /dev/null @@ -1,452 +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 express = require('express'); -const router = express.Router(); -const dleV2Service = require('../services/dleV2Service'); -const logger = require('../utils/logger'); -const auth = require('../middleware/auth'); -const path = require('path'); -const fs = require('fs'); -const ethers = require('ethers'); // Added ethers for private key validation -const create2 = require('../utils/create2'); -const verificationStore = require('../services/verificationStore'); -const etherscanV2 = require('../services/etherscanV2VerificationService'); - -/** - * @route POST /api/dle-v2 - * @desc Создать новое DLE v2 (Digital Legal Entity) - * @access Private (только для авторизованных пользователей с ролью admin) - */ -router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { - try { - const dleParams = req.body; - logger.info('Получен запрос на создание DLE v2:', dleParams); - - // Если параметр initialPartners не был передан явно, используем адрес авторизованного пользователя - if (!dleParams.initialPartners || dleParams.initialPartners.length === 0) { - // Проверяем, есть ли в сессии адрес кошелька пользователя - if (!req.user || !req.user.walletAddress) { - return res.status(400).json({ - success: false, - message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов' - }); - } - - // Используем адрес авторизованного пользователя - dleParams.initialPartners = [req.user.address || req.user.walletAddress]; - - // Если суммы не указаны, используем значение по умолчанию (100% токенов) - if (!dleParams.initialAmounts || dleParams.initialAmounts.length === 0) { - dleParams.initialAmounts = ['1000000000000000000000000']; // 1,000,000 токенов - } - } - - // Создаем DLE v2 - const result = await dleV2Service.createDLE(dleParams); - - logger.info('DLE v2 успешно создано:', result); - - res.json({ - success: true, - message: 'DLE v2 успешно создано', - data: result.data - }); - - } catch (error) { - logger.error('Ошибка при создании DLE v2:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при создании DLE v2' - }); - } -}); - -/** - * @route GET /api/dle-v2 - * @desc Получить список всех DLE v2 - * @access Public (доступно всем пользователям) - */ -router.get('/', async (req, res, next) => { - try { - const dles = dleV2Service.getAllDLEs(); - - res.json({ - success: true, - data: dles - }); - - } catch (error) { - logger.error('Ошибка при получении списка DLE v2:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при получении списка DLE v2' - }); - } -}); - - -/** - * @route GET /api/dle-v2/default-params - * @desc Получить параметры по умолчанию для создания DLE v2 - * @access Private - */ -router.get('/default-params', auth.requireAuth, async (req, res, next) => { - try { - const defaultParams = { - name: '', - symbol: '', - location: '', - coordinates: '', - jurisdiction: 1, - oktmo: 45000000000, - okvedCodes: [], - kpp: 770101001, - quorumPercentage: 51, - initialPartners: [], - initialAmounts: [], - supportedChainIds: [1, 137, 56, 42161], - currentChainId: 1 - }; - - res.json({ - success: true, - data: defaultParams - }); - - } catch (error) { - logger.error('Ошибка при получении параметров по умолчанию:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при получении параметров по умолчанию' - }); - } -}); - -/** - * @route DELETE /api/dle-v2/:dleAddress - * @desc Удалить DLE v2 по адресу - * @access Private (только для авторизованных пользователей с ролью admin) - */ -router.delete('/:dleAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => { - try { - const { dleAddress } = req.params; - logger.info(`Получен запрос на удаление DLE v2 с адресом: ${dleAddress}`); - - // Проверяем существование DLE v2 в директории contracts-data/dles - const dlesDir = path.join(__dirname, '../contracts-data/dles'); - const files = fs.readdirSync(dlesDir); - - let fileToDelete = null; - - // Находим файл, содержащий указанный адрес DLE - for (const file of files) { - if (file.includes('dle-v2-') && file.endsWith('.json')) { - const filePath = path.join(dlesDir, file); - if (fs.statSync(filePath).isFile()) { - try { - const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8')); - if (dleData.dleAddress && dleData.dleAddress.toLowerCase() === dleAddress.toLowerCase()) { - fileToDelete = filePath; - break; - } - } catch (err) { - logger.error(`Ошибка при чтении файла ${file}:`, err); - } - } - } - } - - if (!fileToDelete) { - return res.status(404).json({ - success: false, - message: `DLE v2 с адресом ${dleAddress} не найдено` - }); - } - - // Удаляем файл - fs.unlinkSync(fileToDelete); - - logger.info(`DLE v2 с адресом ${dleAddress} успешно удалено`); - - res.json({ - success: true, - message: `DLE v2 с адресом ${dleAddress} успешно удалено` - }); - - } catch (error) { - logger.error('Ошибка при удалении DLE v2:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при удалении DLE v2' - }); - } -}); - -/** - * @route GET /api/dle-v2/check-admin-tokens - * @desc Проверить баланс админских токенов для адреса - * @access Public - */ -router.get('/check-admin-tokens', async (req, res, next) => { - try { - const { address } = req.query; - - if (!address) { - return res.status(400).json({ - success: false, - message: 'Адрес кошелька не передан' - }); - } - - // Проверяем баланс токенов - const { checkAdminRole } = require('../services/admin-role'); - const isAdmin = await checkAdminRole(address); - - res.json({ - success: true, - data: { - isAdmin: isAdmin, - address: address, - message: isAdmin ? 'Админские токены найдены' : 'Админские токены не найдены' - } - }); - - } catch (error) { - logger.error('Ошибка при проверке админских токенов:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при проверке админских токенов' - }); - } -}); - -/** - * @route POST /api/dle-v2/validate-private-key - * @desc Валидировать приватный ключ и получить адрес кошелька - * @access Public - */ -router.post('/validate-private-key', async (req, res, next) => { - try { - const { privateKey } = req.body; - - if (!privateKey) { - return res.status(400).json({ - success: false, - message: 'Приватный ключ не передан' - }); - } - - // Логируем входящий ключ (только для отладки) - logger.info('Получен приватный ключ для валидации:', privateKey); - logger.info('Длина входящего ключа:', privateKey.length); - logger.info('Тип входящего ключа:', typeof privateKey); - logger.info('Полный объект запроса:', JSON.stringify(req.body)); - - try { - // Очищаем ключ от префикса 0x если есть - const cleanKey = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey; - - // Логируем очищенный ключ (только для отладки) - logger.info('Очищенный ключ:', cleanKey); - logger.info('Длина очищенного ключа:', cleanKey.length); - - // Проверяем длину и формат (64 символа в hex) - if (cleanKey.length !== 64 || !/^[a-fA-F0-9]+$/.test(cleanKey)) { - logger.error('Некорректный формат ключа. Длина:', cleanKey.length, 'Формат:', /^[a-fA-F0-9]+$/.test(cleanKey)); - return res.status(400).json({ - success: false, - message: 'Некорректный формат приватного ключа' - }); - } - - // Генерируем адрес из приватного ключа - const wallet = new ethers.Wallet('0x' + cleanKey); - const address = wallet.address; - - // Логируем сгенерированный адрес - logger.info('Сгенерированный адрес из приватного ключа:', address); - - res.json({ - success: true, - data: { - isValid: true, - address: address, - error: null - } - }); - - } catch (error) { - logger.error('Ошибка при генерации адреса из приватного ключа:', error); - res.status(400).json({ - success: false, - message: 'Некорректный приватный ключ' - }); - } - - } catch (error) { - logger.error('Ошибка при валидации приватного ключа:', error); - res.status(500).json({ - success: false, - message: error.message || 'Произошла ошибка при валидации приватного ключа' - }); - } -}); - -module.exports = router; - -/** - * Дополнительные маршруты (подключаются из app.js) - */ - - -// Сохранить GUID верификации (если нужно отдельным вызовом) -router.post('/verify/save-guid', auth.requireAuth, auth.requireAdmin, async (req, res) => { - try { - const { address, chainId, guid } = req.body || {}; - if (!address || !chainId || !guid) return res.status(400).json({ success: false, message: 'address, chainId, guid обязательны' }); - const data = verificationStore.updateChain(address, chainId, { guid, status: 'submitted' }); - return res.json({ success: true, data }); - } catch (e) { - return res.status(500).json({ success: false, message: e.message }); - } -}); - -// Получить статусы верификации по адресу DLE -router.get('/verify/status/:address', auth.requireAuth, async (req, res) => { - try { - const { address } = req.params; - const data = verificationStore.read(address); - return res.json({ success: true, data }); - } catch (e) { - return res.status(500).json({ success: false, message: e.message }); - } -}); - -// Обновить статусы верификации, опросив Etherscan V2 -router.post('/verify/refresh/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => { - try { - const { address } = req.params; - let { etherscanApiKey } = req.body || {}; - if (!etherscanApiKey) { - try { - const { getSecret } = require('../services/secretStore'); - etherscanApiKey = await getSecret('ETHERSCAN_V2_API_KEY'); - } catch(_) {} - } - const data = verificationStore.read(address); - if (!data || !data.chains) return res.json({ success: true, data }); - - // Если guid отсутствует или ранее была ошибка chainid — попробуем автоматически переотправить верификацию (resubmit) - const needResubmit = Object.values(data.chains).some(c => !c.guid || /Missing or unsupported chainid/i.test(c.status || '')); - if (needResubmit && etherscanApiKey) { - // Найти карточку DLE - const list = dleV2Service.getAllDLEs(); - const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase()); - if (card) { - const deployParams = { - name: card.name, - symbol: card.symbol, - location: card.location, - coordinates: card.coordinates, - jurisdiction: card.jurisdiction, - oktmo: card.oktmo, - okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [], - kpp: card.kpp, - quorumPercentage: card.quorumPercentage, - initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [], - initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [], - supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []), - currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1 - }; - const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } }; - try { - await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); - } catch (_) {} - } - } - - // Далее — обычный опрос по имеющимся guid - const latest = verificationStore.read(address); - const chains = Object.values(latest.chains); - for (const c of chains) { - if (!c.guid || !c.chainId) continue; - try { - const st = await etherscanV2.checkStatus(c.chainId, c.guid, etherscanApiKey); - verificationStore.updateChain(address, c.chainId, { status: st?.result || st?.message || 'unknown' }); - } catch (e) { - verificationStore.updateChain(address, c.chainId, { status: `error: ${e.message}` }); - } - } - const updated = verificationStore.read(address); - return res.json({ success: true, data: updated }); - } catch (e) { - return res.status(500).json({ success: false, message: e.message }); - } -}); - -// Повторно отправить верификацию на Etherscan V2 для уже созданного DLE -router.post('/verify/resubmit/:address', auth.requireAuth, auth.requireAdmin, async (req, res) => { - try { - const { address } = req.params; - const { etherscanApiKey } = req.body || {}; - if (!etherscanApiKey && !process.env.ETHERSCAN_API_KEY) { - return res.status(400).json({ success: false, message: 'etherscanApiKey обязателен' }); - } - // Найти карточку DLE по адресу - const list = dleV2Service.getAllDLEs(); - const card = list.find(x => x?.dleAddress && x.dleAddress.toLowerCase() === address.toLowerCase()); - if (!card) return res.status(404).json({ success: false, message: 'Карточка DLE не найдена' }); - - // Сформировать deployParams из карточки - const deployParams = { - name: card.name, - symbol: card.symbol, - location: card.location, - coordinates: card.coordinates, - jurisdiction: card.jurisdiction, - oktmo: card.oktmo, - okvedCodes: Array.isArray(card.okvedCodes) ? card.okvedCodes : [], - kpp: card.kpp, - quorumPercentage: card.quorumPercentage, - initialPartners: Array.isArray(card.initialPartners) ? card.initialPartners : [], - initialAmounts: Array.isArray(card.initialAmounts) ? card.initialAmounts : [], - supportedChainIds: Array.isArray(card.networks) ? card.networks.map(n => n.chainId).filter(Boolean) : (card.governanceSettings?.supportedChainIds || []), - currentChainId: card.governanceSettings?.currentChainId || (Array.isArray(card.networks) && card.networks[0]?.chainId) || 1 - }; - - // Сформировать deployResult из карточки - const deployResult = { success: true, data: { dleAddress: card.dleAddress, networks: card.networks || [] } }; - - await dleV2Service.autoVerifyAcrossChains({ deployParams, deployResult, apiKey: etherscanApiKey }); - const updated = verificationStore.read(address); - return res.json({ success: true, data: updated }); - } catch (e) { - return res.status(500).json({ success: false, message: e.message }); - } -}); - -// Предварительная проверка балансов во всех выбранных сетях -router.post('/precheck', auth.requireAuth, auth.requireAdmin, async (req, res) => { - try { - const { supportedChainIds, privateKey } = req.body || {}; - if (!privateKey) return res.status(400).json({ success: false, message: 'Приватный ключ не передан' }); - if (!Array.isArray(supportedChainIds) || supportedChainIds.length === 0) { - return res.status(400).json({ success: false, message: 'Не переданы сети для проверки' }); - } - const result = await dleV2Service.checkBalances(supportedChainIds, privateKey); - return res.json({ success: true, data: result }); - } catch (e) { - return res.status(500).json({ success: false, message: e.message }); - } -}); - diff --git a/backup_deploy_20250922_220227/dleV2Service.js b/backup_deploy_20250922_220227/dleV2Service.js deleted file mode 100644 index 9ba53c2..0000000 --- a/backup_deploy_20250922_220227/dleV2Service.js +++ /dev/null @@ -1,971 +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 { spawn } = require('child_process'); -const path = require('path'); -const fs = require('fs'); -const { ethers } = require('ethers'); -const logger = require('../utils/logger'); -const { getRpcUrlByChainId } = require('./rpcProviderService'); -const etherscanV2 = require('./etherscanV2VerificationService'); -const verificationStore = require('./verificationStore'); - -/** - * Сервис для управления DLE v2 (Digital Legal Entity) - * Современный подход с единым контрактом - */ -class DLEV2Service { - /** - * Создает новое DLE v2 с заданными параметрами - * @param {Object} dleParams - Параметры DLE - * @returns {Promise} - Результат создания DLE - */ - async createDLE(dleParams) { - console.log("🔥 [DLEV2-SERVICE] ФУНКЦИЯ createDLE ВЫЗВАНА!"); - logger.info("🚀 DEBUG: ВХОДИМ В createDLE ФУНКЦИЮ"); - let paramsFile = null; - let tempParamsFile = null; - try { - logger.info('Начало создания DLE v2 с параметрами:', dleParams); - - // Валидация входных данных - this.validateDLEParams(dleParams); - - // Подготовка параметров для деплоя - const deployParams = this.prepareDeployParams(dleParams); - - // Вычисляем адрес инициализатора (инициализатором является деплоер из переданного приватного ключа) - try { - const normalizedPk = dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`; - const initializerAddress = new ethers.Wallet(normalizedPk).address; - deployParams.initializerAddress = initializerAddress; - } catch (e) { - logger.warn('Не удалось вычислить initializerAddress из приватного ключа:', e.message); - } - - // Генерируем одноразовый CREATE2_SALT и сохраняем его с уникальным ключом в secrets - const { createAndStoreNewCreate2Salt } = require('./secretStore'); - const { salt: create2Salt, key: saltKey } = await createAndStoreNewCreate2Salt({ label: deployParams.name || 'DLEv2' }); - logger.info(`CREATE2_SALT создан и сохранён: key=${saltKey}`); - - // Сохраняем параметры во временный файл - paramsFile = this.saveParamsToFile(deployParams); - - // Копируем параметры во временный файл с предсказуемым именем - tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json'); - const deployDir = path.dirname(tempParamsFile); - if (!fs.existsSync(deployDir)) { - fs.mkdirSync(deployDir, { recursive: true }); - } - fs.copyFileSync(paramsFile, tempParamsFile); - - // Готовим RPC для всех выбранных сетей - const rpcUrls = []; - for (const cid of deployParams.supportedChainIds) { - logger.info(`Поиск RPC URL для chain_id: ${cid}`); - const ru = await getRpcUrlByChainId(cid); - if (!ru) { - throw new Error(`RPC URL для сети с chain_id ${cid} не найден в базе данных`); - } - rpcUrls.push(ru); - } - - // Добавляем CREATE2_SALT, RPC_URLS и initializer в файл параметров - const currentParams = JSON.parse(fs.readFileSync(tempParamsFile, 'utf8')); - // Копируем все параметры из deployParams - Object.assign(currentParams, deployParams); - currentParams.CREATE2_SALT = create2Salt; - currentParams.rpcUrls = rpcUrls; - currentParams.currentChainId = deployParams.currentChainId || deployParams.supportedChainIds[0]; - const { ethers } = require('ethers'); - currentParams.initializer = dleParams.privateKey ? new ethers.Wallet(dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`).address : "0x0000000000000000000000000000000000000000"; - fs.writeFileSync(tempParamsFile, JSON.stringify(currentParams, null, 2)); - - logger.info(`Файл параметров скопирован и обновлен с CREATE2_SALT`); - - // Лёгкая проверка баланса в первой сети - { - const { ethers } = require('ethers'); - const provider = new ethers.JsonRpcProvider(rpcUrls[0]); - if (dleParams.privateKey) { - const pk = dleParams.privateKey.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`; - const walletAddress = new ethers.Wallet(pk, provider).address; - const balance = await provider.getBalance(walletAddress); - - const minBalance = ethers.parseEther("0.00001"); - logger.info(`Баланс кошелька ${walletAddress}: ${ethers.formatEther(balance)} ETH`); - if (balance < minBalance) { - throw new Error(`Недостаточно ETH для деплоя в ${deployParams.supportedChainIds[0]}. Баланс: ${ethers.formatEther(balance)} ETH`); - } - } - } - if (!dleParams.privateKey) { - throw new Error('Приватный ключ для деплоя не передан'); - } - - // Сначала компилируем контракты - logger.info("🔨 Компилируем контракты перед вычислением INIT_CODE_HASH..."); - try { - const { spawn } = require('child_process'); - await new Promise((resolve, reject) => { - const compile = spawn('npx', ['hardhat', 'compile'], { - cwd: process.cwd(), - stdio: 'inherit' - }); - - compile.on('close', (code) => { - if (code === 0) { - logger.info('✅ Контракты скомпилированы успешно'); - resolve(); - } else { - logger.warn(`⚠️ Компиляция завершилась с кодом: ${code}`); - resolve(); // Продолжаем даже при ошибке компиляции - } - }); - - compile.on('error', (error) => { - logger.warn('⚠️ Ошибка компиляции:', error.message); - resolve(); // Продолжаем даже при ошибке - }); - }); - } catch (compileError) { - logger.warn('⚠️ Ошибка компиляции:', compileError.message); - } - - // INIT_CODE_HASH будет вычислен в deploy-multichain.js - - // Factory больше не используется - деплой DLE напрямую - logger.info(`Подготовка к прямому деплою DLE в сетях: ${deployParams.supportedChainIds.join(', ')}`); - - // Мультисетевой деплой одним вызовом - logger.info('Запуск мульти-чейн деплоя...'); - logger.info("🔍 DEBUG: Подготовка к прямому деплою..."); - - const result = await this.runDeployMultichain(paramsFile, { - rpcUrls: rpcUrls, - chainIds: deployParams.supportedChainIds, - privateKey: dleParams.privateKey?.startsWith('0x') ? dleParams.privateKey : `0x${dleParams.privateKey}`, - salt: create2Salt - }); - - logger.info('Деплой завершен, результат:', JSON.stringify(result, null, 2)); - logger.info("🔍 DEBUG: Запуск мультисетевого деплоя..."); - - // Сохраняем информацию о созданном DLE для отображения на странице управления - try { - logger.info('Результат деплоя для сохранения:', JSON.stringify(result, null, 2)); - - // Проверяем структуру результата - if (!result || typeof result !== 'object') { - logger.error('Неверная структура результата деплоя:', result); - throw new Error('Неверная структура результата деплоя'); - } - logger.info("🔍 DEBUG: Вызываем runDeployMultichain..."); - - // Если результат - массив (прямой результат из скрипта), преобразуем его - let deployResult = result; - if (Array.isArray(result)) { - logger.info('Результат - массив, преобразуем в объект'); - const addresses = result.map(r => r.address); - const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase()); - deployResult = { - success: true, - data: { - dleAddress: addresses[0], - networks: result.map((r, index) => ({ - chainId: r.chainId, - address: r.address, - success: true - })), - allSame - } - }; - } - - const firstNet = Array.isArray(deployResult?.data?.networks) && deployResult.data.networks.length > 0 ? deployResult.data.networks[0] : null; - const dleData = { - name: deployParams.name, - symbol: deployParams.symbol, - location: deployParams.location, - coordinates: deployParams.coordinates, - jurisdiction: deployParams.jurisdiction, - okvedCodes: deployParams.okvedCodes || [], - kpp: deployParams.kpp, - quorumPercentage: deployParams.quorumPercentage, - initialPartners: deployParams.initialPartners || [], - initialAmounts: deployParams.initialAmounts || [], - governanceSettings: { - quorumPercentage: deployParams.quorumPercentage, - supportedChainIds: deployParams.supportedChainIds, - currentChainId: deployParams.currentChainId - }, - dleAddress: (deployResult?.data?.dleAddress) || (firstNet?.address) || null, - version: 'v2', - networks: deployResult?.data?.networks || [], - createdAt: new Date().toISOString() - }; - - // logger.info('Данные DLE для сохранения:', JSON.stringify(dleData, null, 2)); // Убрано избыточное логирование - - if (dleData.dleAddress) { - // Сохраняем данные DLE в файл - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const fileName = `dle-v2-${timestamp}.json`; - const savedPath = path.join(__dirname, '../contracts-data/dles', fileName); - - // Создаем директорию, если её нет - const dlesDir = path.dirname(savedPath); - if (!fs.existsSync(dlesDir)) { - fs.mkdirSync(dlesDir, { recursive: true }); - } - - fs.writeFileSync(savedPath, JSON.stringify(dleData, null, 2)); - // logger.info(`DLE данные сохранены в: ${savedPath}`); // Убрано избыточное логирование - - return { - success: true, - data: dleData - }; - } else { - throw new Error('DLE адрес не получен после деплоя'); - } - } catch (e) { - logger.warn('Не удалось сохранить локальную карточку DLE:', e.message); - } - - // Сохраняем ключ Etherscan V2 для последующих авто‑обновлений статуса, если он передан - try { - if (dleParams.etherscanApiKey) { - const { setSecret } = require('./secretStore'); - await setSecret('ETHERSCAN_V2_API_KEY', dleParams.etherscanApiKey); - } - } catch (_) {} - - // Верификация выполняется в deploy-multichain.js - - return result; - - } catch (error) { - logger.error('Ошибка при создании DLE v2:', error); - throw error; - } finally { - try { - if (paramsFile || tempParamsFile) { - this.cleanupTempFiles(paramsFile, tempParamsFile); - } - } catch (e) { - logger.warn('Ошибка при очистке временных файлов (finally):', e.message); - } - try { - this.pruneOldTempFiles(24 * 60 * 60 * 1000); - } catch (e) { - logger.warn('Ошибка при автоочистке старых временных файлов:', e.message); - } - } - } - - /** - * Валидирует параметры DLE - * @param {Object} params - Параметры для валидации - */ - validateDLEParams(params) { - if (!params.name || params.name.trim() === '') { - throw new Error('Название DLE обязательно'); - } - - if (!params.symbol || params.symbol.trim() === '') { - throw new Error('Символ токена обязателен'); - } - - if (!params.location || params.location.trim() === '') { - throw new Error('Местонахождение DLE обязательно'); - } - - if (!params.initialPartners || !Array.isArray(params.initialPartners)) { - throw new Error('Партнеры должны быть массивом'); - } - - if (!params.initialAmounts || !Array.isArray(params.initialAmounts)) { - throw new Error('Суммы должны быть массивом'); - } - - if (params.initialPartners.length !== params.initialAmounts.length) { - throw new Error('Количество партнеров должно соответствовать количеству сумм распределения'); - } - - if (params.initialPartners.length === 0) { - throw new Error('Должен быть указан хотя бы один партнер'); - } - - if (params.quorumPercentage > 100 || params.quorumPercentage < 1) { - throw new Error('Процент кворума должен быть от 1% до 100%'); - } - - // Проверяем адреса партнеров - for (let i = 0; i < params.initialPartners.length; i++) { - if (!ethers.isAddress || !ethers.isAddress(params.initialPartners[i])) { - throw new Error(`Неверный адрес партнера ${i + 1}: ${params.initialPartners[i]}`); - } - } - - // Проверяем, что выбраны сети - if (!params.supportedChainIds || !Array.isArray(params.supportedChainIds) || params.supportedChainIds.length === 0) { - throw new Error('Должна быть выбрана хотя бы одна сеть для деплоя'); - } - - // Дополнительные проверки безопасности - if (params.name.length > 100) { - throw new Error('Название DLE слишком длинное (максимум 100 символов)'); - } - - if (params.symbol.length > 10) { - throw new Error('Символ токена слишком длинный (максимум 10 символов)'); - } - - if (params.location.length > 200) { - throw new Error('Местонахождение слишком длинное (максимум 200 символов)'); - } - - // Проверяем суммы токенов - for (let i = 0; i < params.initialAmounts.length; i++) { - const amount = params.initialAmounts[i]; - if (typeof amount !== 'string' && typeof amount !== 'number') { - throw new Error(`Неверный тип суммы для партнера ${i + 1}`); - } - - const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; - if (isNaN(numAmount) || numAmount <= 0) { - throw new Error(`Неверная сумма для партнера ${i + 1}: ${amount}`); - } - } - - // Проверяем приватный ключ - if (!params.privateKey) { - throw new Error('Приватный ключ обязателен для деплоя'); - } - - const pk = params.privateKey.startsWith('0x') ? params.privateKey : `0x${params.privateKey}`; - if (!/^0x[a-fA-F0-9]{64}$/.test(pk)) { - throw new Error('Неверный формат приватного ключа'); - } - - // Проверяем, что не деплоим в mainnet без подтверждения - const mainnetChains = [1, 137, 56, 42161]; // Ethereum, Polygon, BSC, Arbitrum - const hasMainnet = params.supportedChainIds.some(id => mainnetChains.includes(id)); - - if (hasMainnet) { - logger.warn('⚠️ ВНИМАНИЕ: Деплой включает mainnet сети! Убедитесь, что это необходимо.'); - } - - logger.info('✅ Валидация параметров DLE пройдена успешно'); - } - - /** - * Сохраняет/обновляет локальную карточку DLE для отображения в UI - * @param {Object} dleData - * @returns {string} Путь к сохраненному файлу - */ - saveDLEData(dleData) { - try { - if (!dleData || !dleData.dleAddress) { - throw new Error('Неверные данные для сохранения карточки DLE: отсутствует dleAddress'); - } - const dlesDir = path.join(__dirname, '../contracts-data/dles'); - if (!fs.existsSync(dlesDir)) { - fs.mkdirSync(dlesDir, { recursive: true }); - } - - // Если уже есть файл с таким адресом — обновим его - let targetFile = null; - try { - const files = fs.readdirSync(dlesDir); - for (const file of files) { - if (file.endsWith('.json') && file.includes('dle-v2-')) { - const fp = path.join(dlesDir, file); - try { - const existing = JSON.parse(fs.readFileSync(fp, 'utf8')); - if (existing?.dleAddress && existing.dleAddress.toLowerCase() === dleData.dleAddress.toLowerCase()) { - targetFile = fp; - // Совмещаем данные (не удаляя существующие поля сетей/верификации, если присутствуют) - dleData = { ...existing, ...dleData }; - break; - } - } catch (_) {} - } - } - } catch (_) {} - - if (!targetFile) { - const ts = new Date().toISOString().replace(/[:.]/g, '-'); - const fileName = `dle-v2-${ts}.json`; - targetFile = path.join(dlesDir, fileName); - } - - fs.writeFileSync(targetFile, JSON.stringify(dleData, null, 2)); - logger.info(`Карточка DLE сохранена: ${targetFile}`); - return targetFile; - } catch (e) { - logger.error('Ошибка сохранения карточки DLE:', e); - throw e; - } - } - - /** - * Подготавливает параметры для деплоя - * @param {Object} params - Параметры DLE из формы - * @returns {Object} - Подготовленные параметры для скрипта деплоя - */ - prepareDeployParams(params) { - // Создаем копию объекта, чтобы не изменять исходный - const deployParams = { ...params }; - - // Преобразуем суммы из строк или чисел в BigNumber, если нужно - if (deployParams.initialAmounts && Array.isArray(deployParams.initialAmounts)) { - deployParams.initialAmounts = deployParams.initialAmounts.map(rawAmount => { - // Принимаем как строки, так и числа; конвертируем в base units (18 знаков) - try { - if (typeof rawAmount === 'number' && Number.isFinite(rawAmount)) { - return ethers.parseUnits(rawAmount.toString(), 18).toString(); - } - if (typeof rawAmount === 'string') { - const a = rawAmount.trim(); - if (a.startsWith('0x')) { - // Уже base units (hex BigNumber) — оставляем как есть - return BigInt(a).toString(); - } - // Десятичная строка — конвертируем в base units - return ethers.parseUnits(a, 18).toString(); - } - // BigInt или иные типы — приводим к строке без изменения масштаба - return rawAmount.toString(); - } catch (e) { - // Фолбэк: безопасно привести к строке - return String(rawAmount); - } - }); - } - - // Убеждаемся, что okvedCodes - это массив - if (!Array.isArray(deployParams.okvedCodes)) { - deployParams.okvedCodes = []; - } - - // Преобразуем kpp в число - if (deployParams.kpp) { - deployParams.kpp = parseInt(deployParams.kpp) || 0; - } else { - deployParams.kpp = 0; - } - - // Убеждаемся, что supportedChainIds - это массив - if (!Array.isArray(deployParams.supportedChainIds)) { - deployParams.supportedChainIds = [1]; // По умолчанию Ethereum - } - - // Устанавливаем currentChainId как первую выбранную сеть - if (deployParams.supportedChainIds.length > 0) { - deployParams.currentChainId = deployParams.supportedChainIds[0]; - } else { - deployParams.currentChainId = 1; // По умолчанию Ethereum - } - - // Обрабатываем logoURI - if (deployParams.logoURI) { - // Если logoURI относительный путь, делаем его абсолютным - if (deployParams.logoURI.startsWith('/uploads/')) { - deployParams.logoURI = `http://localhost:8000${deployParams.logoURI}`; - } - // Если это placeholder, оставляем как есть - if (deployParams.logoURI.includes('placeholder.com')) { - // Оставляем как есть - } - } - - return deployParams; - } - - /** - * Сохраняет параметры во временный файл - * @param {Object} params - Параметры для сохранения - * @returns {string} - Путь к сохраненному файлу - */ - saveParamsToFile(params) { - const tempDir = path.join(__dirname, '../temp'); - - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir, { recursive: true }); - } - - const fileName = `dle-v2-params-${Date.now()}.json`; - const filePath = path.join(tempDir, fileName); - - fs.writeFileSync(filePath, JSON.stringify(params, null, 2)); - - return filePath; - } - - /** - * Запускает скрипт деплоя DLE v2 - * @param {string} paramsFile - Путь к файлу с параметрами - * @returns {Promise} - Результат деплоя - */ - runDeployScript(paramsFile, extraEnv = {}) { - return new Promise((resolve, reject) => { - const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js'); - if (!fs.existsSync(scriptPath)) { - reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath)); - return; - } - - const envVars = { - ...process.env, - RPC_URL: extraEnv.rpcUrl, - PRIVATE_KEY: extraEnv.privateKey - }; - - const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath], { - cwd: path.join(__dirname, '..'), - env: envVars, - stdio: ['inherit', 'pipe', 'pipe'] - }); - - let stdout = ''; - let stderr = ''; - - hardhatProcess.stdout.on('data', (data) => { - stdout += data.toString(); - logger.info(`[DLE v2 Deploy] ${data.toString().trim()}`); - }); - - hardhatProcess.stderr.on('data', (data) => { - stderr += data.toString(); - logger.error(`[DLE v2 Deploy Error] ${data.toString().trim()}`); - }); - - hardhatProcess.on('close', (code) => { - try { - const result = this.extractDeployResult(stdout); - resolve(result); - } catch (error) { - logger.error('Ошибка при извлечении результатов деплоя DLE v2:', error); - if (code === 0) { - reject(new Error('Не удалось найти информацию о созданном DLE v2')); - } else { - reject(new Error(`Скрипт деплоя DLE v2 завершился с кодом ${code}: ${stderr}`)); - } - } - }); - - hardhatProcess.on('error', (error) => { - logger.error('Ошибка запуска скрипта деплоя DLE v2:', error); - reject(error); - }); - }); - } - - // Мультисетевой деплой - runDeployMultichain(paramsFile, opts = {}) { - return new Promise((resolve, reject) => { - const scriptPath = path.join(__dirname, '../scripts/deploy/deploy-multichain.js'); - if (!fs.existsSync(scriptPath)) return reject(new Error('Скрипт мультисетевого деплоя не найден: ' + scriptPath)); - - const envVars = { - ...process.env, - PRIVATE_KEY: opts.privateKey - }; - - const p = spawn('npx', ['hardhat', 'run', scriptPath], { - cwd: path.join(__dirname, '..'), - env: envVars, - stdio: ['inherit', 'pipe', 'pipe'] - }); - - let stdout = '', stderr = ''; - p.stdout.on('data', (d) => { - stdout += d.toString(); - logger.info(`[MULTICHAIN_DEPLOY] ${d.toString().trim()}`); - }); - p.stderr.on('data', (d) => { - stderr += d.toString(); - logger.error(`[MULTICHAIN_DEPLOY_ERR] ${d.toString().trim()}`); - }); - - p.on('close', (code) => { - try { - // Ищем результат в формате MULTICHAIN_DEPLOY_RESULT - const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*\])/); - - if (resultMatch) { - const deployResults = JSON.parse(resultMatch[1]); - - // Преобразуем результат в нужный формат - const addresses = deployResults.map(r => r.address); - const allSame = addresses.every(addr => addr.toLowerCase() === addresses[0].toLowerCase()); - - resolve({ - success: true, - data: { - dleAddress: addresses[0], - networks: deployResults.map((r, index) => ({ - chainId: r.chainId, - address: r.address, - success: true - })), - allSame - } - }); - } else { - // Fallback: ищем адреса DLE в выводе по новому формату - const dleAddressMatches = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/g); - if (!dleAddressMatches || dleAddressMatches.length === 0) { - throw new Error('Не найдены адреса DLE в выводе'); - } - - const addresses = dleAddressMatches.map(match => match.match(/(0x[a-fA-F0-9]{40})/)[1]); - const addr = addresses[0]; - const allSame = addresses.every(x => x.toLowerCase() === addr.toLowerCase()); - - if (!allSame) { - logger.warn('Адреса отличаются между сетями — продолжаем, сохраню по-сеточно', { addresses }); - } - - resolve({ - success: true, - data: { - dleAddress: addr, - networks: addresses.map((address, index) => ({ - chainId: opts.chainIds[index] || index + 1, - address, - success: true - })), - allSame - } - }); - } - } catch (e) { - reject(new Error(`Ошибка мультисетевого деплоя: ${e.message}\nSTDOUT:${stdout}\nSTDERR:${stderr}`)); - } - }); - - p.on('error', (e) => reject(e)); - }); - } - - /** - * Извлекает результат деплоя из stdout - * @param {string} stdout - Вывод скрипта - * @returns {Object} - Результат деплоя - */ - extractDeployResult(stdout) { - // Ищем результат в формате MULTICHAIN_DEPLOY_RESULT - const resultMatch = stdout.match(/MULTICHAIN_DEPLOY_RESULT\s+(\[.*?\])/); - - if (resultMatch) { - try { - const result = JSON.parse(resultMatch[1]); - return result; - } catch (e) { - logger.error('Ошибка парсинга JSON результата:', e); - } - } - - // Fallback: ищем строки с адресами в выводе по новому формату - const dleAddressMatch = stdout.match(/\[MULTI_DBG\] chainId=\d+ DLE deployed at=(0x[a-fA-F0-9]{40})/); - - if (dleAddressMatch) { - return { - success: true, - data: { - dleAddress: dleAddressMatch[1], - version: 'v2' - } - }; - } - - // Если не нашли адрес, выводим весь stdout для отладки - console.log('Полный вывод скрипта:', stdout); - throw new Error('Не удалось извлечь адрес DLE из вывода скрипта'); - } - - /** - * Очищает временные файлы - * @param {string} paramsFile - Путь к файлу параметров - * @param {string} tempParamsFile - Путь к временному файлу параметров - */ - cleanupTempFiles(paramsFile, tempParamsFile) { - try { - if (fs.existsSync(paramsFile)) { - fs.unlinkSync(paramsFile); - } - if (fs.existsSync(tempParamsFile)) { - fs.unlinkSync(tempParamsFile); - } - } catch (error) { - logger.warn('Не удалось очистить временные файлы:', error); - } - } - - /** - * Удаляет временные файлы параметров деплоя старше заданного возраста - * @param {number} maxAgeMs - Макс. возраст файлов в миллисекундах (по умолчанию 24ч) - */ - pruneOldTempFiles(maxAgeMs = 24 * 60 * 60 * 1000) { - const tempDir = path.join(__dirname, '../temp'); - try { - if (!fs.existsSync(tempDir)) return; - const now = Date.now(); - const files = fs.readdirSync(tempDir).filter(f => f.startsWith('dle-v2-params-') && f.endsWith('.json')); - for (const f of files) { - const fp = path.join(tempDir, f); - try { - const st = fs.statSync(fp); - if (now - st.mtimeMs > maxAgeMs) { - fs.unlinkSync(fp); - logger.info(`Удалён старый временный файл: ${fp}`); - } - } catch (e) { - logger.warn(`Не удалось обработать файл ${fp}: ${e.message}`); - } - } - } catch (e) { - logger.warn('Ошибка pruneOldTempFiles:', e.message); - } - } - - /** - * Получает список всех созданных DLE v2 - * @returns {Array} - Список DLE v2 - */ - getAllDLEs() { - try { - const dlesDir = path.join(__dirname, '../contracts-data/dles'); - - if (!fs.existsSync(dlesDir)) { - return []; - } - - const files = fs.readdirSync(dlesDir); - const allDles = files - .filter(file => file.endsWith('.json') && file.includes('dle-v2-')) - .map(file => { - try { - const data = JSON.parse(fs.readFileSync(path.join(dlesDir, file), 'utf8')); - return { ...data, _fileName: file }; - } catch (error) { - logger.error(`Ошибка при чтении файла ${file}:`, error); - return null; - } - }) - .filter(dle => dle !== null); - - // Группируем DLE по мультичейн деплоям - const groupedDles = this.groupMultichainDLEs(allDles); - - return groupedDles; - } catch (error) { - logger.error('Ошибка при получении списка DLE v2:', error); - return []; - } - } - - /** - * Группирует DLE по мультичейн деплоям - * @param {Array} allDles - Все DLE из файлов - * @returns {Array} - Сгруппированные DLE - */ - groupMultichainDLEs(allDles) { - const groups = new Map(); - - for (const dle of allDles) { - // Создаем ключ для группировки на основе общих параметров - const groupKey = this.createGroupKey(dle); - - if (!groups.has(groupKey)) { - groups.set(groupKey, { - // Основные данные из первого DLE - name: dle.name, - symbol: dle.symbol, - location: dle.location, - coordinates: dle.coordinates, - jurisdiction: dle.jurisdiction, - oktmo: dle.oktmo, - okvedCodes: dle.okvedCodes, - kpp: dle.kpp, - quorumPercentage: dle.quorumPercentage, - version: dle.version || 'v2', - deployedMultichain: true, - // Мультичейн информация - networks: [], - // Модули (одинаковые во всех сетях) - modules: dle.modules, - // Время создания (самое раннее) - creationTimestamp: dle.creationTimestamp, - creationBlock: dle.creationBlock - }); - } - - const group = groups.get(groupKey); - - // Если у DLE есть массив networks, используем его - if (dle.networks && Array.isArray(dle.networks)) { - for (const network of dle.networks) { - group.networks.push({ - chainId: network.chainId, - dleAddress: network.address || network.dleAddress, - factoryAddress: network.factoryAddress, - rpcUrl: network.rpcUrl || this.getRpcUrlForChain(network.chainId) - }); - } - } else { - // Старый формат: добавляем информацию о сети из корня DLE - group.networks.push({ - chainId: dle.chainId, - dleAddress: dle.dleAddress, - factoryAddress: dle.factoryAddress, - rpcUrl: dle.rpcUrl || this.getRpcUrlForChain(dle.chainId) - }); - } - - // Обновляем время создания на самое раннее - if (dle.creationTimestamp && (!group.creationTimestamp || dle.creationTimestamp < group.creationTimestamp)) { - group.creationTimestamp = dle.creationTimestamp; - } - } - - // Преобразуем группы в массив - return Array.from(groups.values()).map(group => ({ - ...group, - // Основной адрес DLE (из первой сети) - dleAddress: group.networks[0]?.dleAddress, - // Общее количество сетей - totalNetworks: group.networks.length, - // Поддерживаемые сети - supportedChainIds: group.networks.map(n => n.chainId) - })); - } - - /** - * Создает ключ для группировки DLE - * @param {Object} dle - Данные DLE - * @returns {string} - Ключ группировки - */ - createGroupKey(dle) { - // Группируем по основным параметрам DLE - const keyParts = [ - dle.name, - dle.symbol, - dle.location, - dle.coordinates, - dle.jurisdiction, - dle.oktmo, - dle.kpp, - dle.quorumPercentage, - // Сортируем okvedCodes для стабильного ключа - Array.isArray(dle.okvedCodes) ? dle.okvedCodes.sort().join(',') : '', - // Сортируем supportedChainIds для стабильного ключа - Array.isArray(dle.supportedChainIds) ? dle.supportedChainIds.sort().join(',') : '' - ]; - - return keyParts.join('|'); - } - - /** - * Получает RPC URL для сети - * @param {number} chainId - ID сети - * @returns {string|null} - RPC URL - */ - getRpcUrlForChain(chainId) { - try { - // Простая маппинг для основных сетей - const rpcMap = { - 1: 'https://eth-mainnet.g.alchemy.com/v2/demo', - 11155111: 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', - 17000: 'https://ethereum-holesky.publicnode.com', - 421614: 'https://sepolia-rollup.arbitrum.io/rpc', - 84532: 'https://sepolia.base.org' - }; - return rpcMap[chainId] || null; - } catch (error) { - return null; - } - } - - - - - /** - * Проверяет балансы в указанных сетях - * @param {number[]} chainIds - Массив chainId для проверки - * @param {string} privateKey - Приватный ключ - * @returns {Promise} - Результат проверки балансов - */ - async checkBalances(chainIds, privateKey) { - const { getRpcUrlByChainId } = require('./rpcProviderService'); - const { ethers } = require('ethers'); - const balances = []; - const insufficient = []; - - for (const chainId of chainIds) { - try { - const rpcUrl = await getRpcUrlByChainId(chainId); - if (!rpcUrl) { - balances.push({ - chainId, - balanceEth: '0', - ok: false, - error: 'RPC URL не найден' - }); - insufficient.push(chainId); - continue; - } - - const provider = new ethers.JsonRpcProvider(rpcUrl); - const wallet = new ethers.Wallet(privateKey, provider); - const balance = await provider.getBalance(wallet.address); - - const balanceEth = ethers.formatEther(balance); - const minBalance = ethers.parseEther("0.001"); - const ok = balance >= minBalance; - - balances.push({ - chainId, - address: wallet.address, - balanceEth, - ok - }); - - if (!ok) { - insufficient.push(chainId); - } - - } catch (error) { - balances.push({ - chainId, - balanceEth: '0', - ok: false, - error: error.message - }); - insufficient.push(chainId); - } - } - - return { - balances, - insufficient, - allSufficient: insufficient.length === 0 - }; - } - - -} - -module.exports = new DLEV2Service(); \ No newline at end of file diff --git a/frontend/src/components/ContactTable.vue b/frontend/src/components/ContactTable.vue index 83b6232..5f34ac5 100644 --- a/frontend/src/components/ContactTable.vue +++ b/frontend/src/components/ContactTable.vue @@ -13,10 +13,10 @@