From 1b387a6d5d295d25219c70f2fb4f11f829c573bc Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 30 Sep 2025 16:41:15 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=B0=D1=88=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/moduleDeployment.js | 55 +++++ .../modules-deploy-summary.json | 78 ++++++ backend/scripts/deploy/deploy-modules.js | 36 ++- backend/scripts/deploy/deploy-multichain.js | 160 ++++-------- .../services/deploymentWebSocketService.js | 24 +- .../temp-module-args-hierarchicalVoting.js | 3 + backend/utils/deploymentUtils.js | 105 +------- backend/utils/nonceManager.js | 122 +++++---- backend/utils/rpcConnectionManager.js | 35 +-- backend/wsHub.js | 12 + .../deployment/DeploymentWizard.vue | 19 +- .../src/composables/useDeploymentWebSocket.js | 88 ++++++- frontend/src/utils/websocket.js | 70 +++++- .../src/views/smartcontracts/ModulesView.vue | 232 +++++++----------- 14 files changed, 570 insertions(+), 469 deletions(-) create mode 100644 backend/scripts/contracts-data/modules-deploy-summary.json create mode 100644 backend/temp-module-args-hierarchicalVoting.js diff --git a/backend/routes/moduleDeployment.js b/backend/routes/moduleDeployment.js index 80c7970..a0707d5 100644 --- a/backend/routes/moduleDeployment.js +++ b/backend/routes/moduleDeployment.js @@ -51,6 +51,28 @@ router.post('/deploy', async (req, res) => { deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Начинаем деплой модуля ${moduleType}`); deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', `Запускаем Hardhat скрипт деплоя...`); + // Отправляем сообщение о начале деплоя + deploymentWebSocketService.broadcastToDLE(dleAddress, { + type: 'deployment_started', + dleAddress: dleAddress, + moduleType: moduleType, + status: 'starting', + progress: 0, + step: 1, + message: `Начинаем деплой модуля ${moduleType}` + }); + + // Отправляем статус начала деплоя + deploymentWebSocketService.broadcastToDLE(dleAddress, { + type: 'deployment_status', + dleAddress: dleAddress, + moduleType: moduleType, + status: 'starting', + progress: 10, + step: 1, + message: 'Инициализация деплоя...' + }); + const child = spawn('npx', ['hardhat', 'run', 'scripts/deploy/deploy-modules.js'], { cwd: path.join(__dirname, '..'), stdio: 'pipe', @@ -71,6 +93,39 @@ router.post('/deploy', async (req, res) => { // Отправляем логи через WebSocket deploymentWebSocketService.addDeploymentLog(dleAddress, 'info', output.trim()); + + // Анализируем логи и обновляем прогресс + if (output.includes('Compiling') || output.includes('Compilation')) { + deploymentWebSocketService.broadcastToDLE(dleAddress, { + type: 'deployment_status', + dleAddress: dleAddress, + moduleType: moduleType, + status: 'compiling', + progress: 30, + step: 2, + message: 'Компиляция контрактов...' + }); + } else if (output.includes('Deploying') || output.includes('deploying')) { + deploymentWebSocketService.broadcastToDLE(dleAddress, { + type: 'deployment_status', + dleAddress: dleAddress, + moduleType: moduleType, + status: 'deploying', + progress: 50, + step: 3, + message: 'Деплой в сетях...' + }); + } else if (output.includes('verify') || output.includes('verification')) { + deploymentWebSocketService.broadcastToDLE(dleAddress, { + type: 'deployment_status', + dleAddress: dleAddress, + moduleType: moduleType, + status: 'verifying', + progress: 80, + step: 4, + message: 'Верификация контрактов...' + }); + } }); child.stderr.on('data', (data) => { diff --git a/backend/scripts/contracts-data/modules-deploy-summary.json b/backend/scripts/contracts-data/modules-deploy-summary.json new file mode 100644 index 0000000..a08ffb1 --- /dev/null +++ b/backend/scripts/contracts-data/modules-deploy-summary.json @@ -0,0 +1,78 @@ +{ + "deploymentId": "modules-deploy-1759239534343", + "dleAddress": "0x7DB7E55eA9050105cDeeC892A1B8D4Ea335b0BFD", + "dleName": "DLE-8", + "dleSymbol": "TOKEN", + "dleLocation": "101000, Москва, Москва, тверская, 1, 1", + "dleJurisdiction": 643, + "dleCoordinates": "55.7715511,37.5929598", + "dleOktmo": "45000000", + "dleOkvedCodes": [ + "63.11", + "62.01" + ], + "dleKpp": "773009001", + "dleLogoURI": "/uploads/logos/default-token.svg", + "dleSupportedChainIds": [ + 11155111, + 84532, + 17000, + 421614 + ], + "totalNetworks": 4, + "successfulNetworks": 4, + "modulesDeployed": [ + "reader" + ], + "networks": [ + { + "chainId": 11155111, + "rpcUrl": "https://1rpc.io/sepolia", + "modules": [ + { + "type": "reader", + "address": "0xE56984dD05E3389AE7EfB3e12A8084915B776ACc", + "success": true, + "verification": "verified" + } + ] + }, + { + "chainId": 84532, + "rpcUrl": "https://sepolia.base.org", + "modules": [ + { + "type": "reader", + "address": "0xE56984dD05E3389AE7EfB3e12A8084915B776ACc", + "success": true, + "verification": "verified" + } + ] + }, + { + "chainId": 17000, + "rpcUrl": "https://ethereum-holesky.publicnode.com", + "modules": [ + { + "type": "reader", + "address": "0xE56984dD05E3389AE7EfB3e12A8084915B776ACc", + "success": true, + "verification": "verified" + } + ] + }, + { + "chainId": 421614, + "rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc", + "modules": [ + { + "type": "reader", + "address": "0xE56984dD05E3389AE7EfB3e12A8084915B776ACc", + "success": true, + "verification": "verified" + } + ] + } + ], + "timestamp": "2025-09-30T13:38:54.344Z" +} \ No newline at end of file diff --git a/backend/scripts/deploy/deploy-modules.js b/backend/scripts/deploy/deploy-modules.js index 127ea9d..38c86f5 100644 --- a/backend/scripts/deploy/deploy-modules.js +++ b/backend/scripts/deploy/deploy-modules.js @@ -15,7 +15,8 @@ const hre = require('hardhat'); const path = require('path'); const fs = require('fs'); const logger = require('../../utils/logger'); -const { getFeeOverrides, createProviderAndWallet, alignNonce, getNetworkInfo, createRPCConnection, sendTransactionWithRetry, createMultipleRPCConnections } = require('../../utils/deploymentUtils'); +const { getFeeOverrides, createProviderAndWallet, getNetworkInfo, createRPCConnection, createMultipleRPCConnections } = require('../../utils/deploymentUtils'); +const RPCConnectionManager = require('../../utils/rpcConnectionManager'); const { nonceManager } = require('../../utils/nonceManager'); // WebSocket сервис удален - логи отправляются через главный процесс @@ -40,10 +41,10 @@ const MODULE_CONFIGS = { }, timelock: { contractName: 'TimelockModule', - constructorArgs: (dleAddress) => [ + constructorArgs: (dleAddress, chainId, walletAddress) => [ dleAddress // _dleContract ], - verificationArgs: (dleAddress) => [ + verificationArgs: (dleAddress, chainId, walletAddress) => [ dleAddress // _dleContract ] }, @@ -58,10 +59,10 @@ const MODULE_CONFIGS = { }, hierarchicalVoting: { contractName: 'HierarchicalVotingModule', - constructorArgs: (dleAddress) => [ + constructorArgs: (dleAddress, chainId, walletAddress) => [ dleAddress // _dleContract ], - verificationArgs: (dleAddress) => [ + verificationArgs: (dleAddress, chainId, walletAddress) => [ dleAddress // _dleContract ] } @@ -189,7 +190,6 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploying ${moduleType}...`); // 1) Используем NonceManager для правильного управления nonce - const { nonceManager } = require('../../utils/nonceManager'); const chainId = Number(net.chainId); let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId); logger.info(`[MODULES_DBG] chainId=${chainId} current nonce=${current} target=${targetNonce}`); @@ -220,7 +220,8 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce ...overrides }; logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}`); - const { tx: txFill, receipt } = await sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 }); + const rpcManager = new RPCConnectionManager(); + const { tx: txFill, receipt } = await rpcManager.sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 }); logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); sent = true; @@ -235,7 +236,6 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce if (String(e?.message || '').toLowerCase().includes('nonce too low') && attempt < 2) { // Сбрасываем кэш и получаем актуальный nonce - const { nonceManager } = require('../../utils/nonceManager'); nonceManager.resetNonce(wallet.address, Number(net.chainId)); current = await provider.getTransactionCount(wallet.address, 'pending'); logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} updated nonce to ${current}`); @@ -322,7 +322,8 @@ async function deployModuleInNetwork(rpcUrl, pk, salt, initCodeHash, targetNonce ...feeOverrides }; - const result = await sendTransactionWithRetry(wallet, txData, { maxRetries: 3 }); + const rpcManager = new RPCConnectionManager(); + const result = await rpcManager.sendTransactionWithRetry(wallet, txData, { maxRetries: 3 }); tx = result.tx; logger.info(`[MODULES_DBG] chainId=${Number(net.chainId)} deploy successful on attempt ${deployAttempts}`); @@ -459,6 +460,23 @@ async function deployAllModulesInNetwork(rpcUrl, pk, salt, dleAddress, modulesTo const moduleConfig = MODULE_CONFIGS[moduleType]; const constructorArgs = moduleConfig.constructorArgs(dleAddress, Number(net.chainId), wallet.address); + // Ждем 30 секунд перед верификацией, чтобы транзакция получила подтверждения + logger.info(`[MODULES_DBG] Ждем 30 секунд перед верификацией модуля ${moduleType}...`); + await new Promise(resolve => setTimeout(resolve, 30000)); + + // Проверяем, что контракт действительно задеплоен + try { + const { provider } = await createRPCConnection(rpcUrl, pk, { maxRetries: 3, timeout: 30000 }); + const code = await provider.getCode(result.address); + if (!code || code === '0x') { + logger.warn(`[MODULES_DBG] Контракт ${moduleType} не найден по адресу ${result.address}, пропускаем верификацию`); + return { success: false, error: 'Контракт не найден' }; + } + logger.info(`[MODULES_DBG] Контракт ${moduleType} найден, код: ${code.substring(0, 20)}...`); + } catch (checkError) { + logger.warn(`[MODULES_DBG] Ошибка проверки контракта: ${checkError.message}`); + } + const verificationResult = await verifyModuleAfterDeploy( Number(net.chainId), result.address, diff --git a/backend/scripts/deploy/deploy-multichain.js b/backend/scripts/deploy/deploy-multichain.js index 1a73912..13153a7 100755 --- a/backend/scripts/deploy/deploy-multichain.js +++ b/backend/scripts/deploy/deploy-multichain.js @@ -36,7 +36,8 @@ const logger = require('../../utils/logger'); console.log('[MULTI_DBG] ✅ logger импортирован'); console.log('[MULTI_DBG] 📦 Импортируем deploymentUtils...'); -const { getFeeOverrides, createProviderAndWallet, alignNonce, getNetworkInfo, createMultipleRPCConnections, sendTransactionWithRetry, createRPCConnection } = require('../../utils/deploymentUtils'); +const { getFeeOverrides, createProviderAndWallet, getNetworkInfo, createMultipleRPCConnections, createRPCConnection } = require('../../utils/deploymentUtils'); +const RPCConnectionManager = require('../../utils/rpcConnectionManager'); console.log('[MULTI_DBG] ✅ deploymentUtils импортирован'); console.log('[MULTI_DBG] 📦 Импортируем nonceManager...'); @@ -243,132 +244,49 @@ async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit logger.error('[MULTI_DBG] precheck error', e?.message || e); } - // 1) Используем NonceManager для правильного управления nonce + // 1) Используем NonceManager для получения актуального nonce const chainId = Number(net.chainId); - let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId); - logger.info(`[MULTI_DBG] chainId=${chainId} current nonce=${current} target=${targetDLENonce}`); + let current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); + logger.info(`[MULTI_DBG] chainId=${chainId} current nonce=${current} (target was ${targetDLENonce})`); + // Если текущий nonce больше целевого, обновляем targetDLENonce if (current > targetDLENonce) { - logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > targetDLENonce ${targetDLENonce} - waiting for sync`); - - // Ждем синхронизации nonce (максимум 60 секунд с прогрессивной задержкой) - let waitTime = 0; - let checkInterval = 1000; // Начинаем с 1 секунды - - while (current > targetDLENonce && waitTime < 60000) { - await new Promise(resolve => setTimeout(resolve, checkInterval)); - current = await nonceManager.getNonce(wallet.address, rpcUrl, chainId); - waitTime += checkInterval; - - // Прогрессивно увеличиваем интервал проверки - if (waitTime > 10000) checkInterval = 2000; - if (waitTime > 30000) checkInterval = 5000; - - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} waiting for nonce sync: ${current} > ${targetDLENonce} (${waitTime}ms, next check in ${checkInterval}ms)`); - } - - if (current > targetDLENonce) { - const errorMsg = `Nonce sync timeout: current ${current} > targetDLENonce ${targetDLENonce} on chainId=${Number(net.chainId)}. This may indicate network issues or the wallet was used for other transactions.`; - logger.error(`[MULTI_DBG] ${errorMsg}`); - throw new Error(errorMsg); - } - - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce sync completed: ${current} <= ${targetDLENonce}`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > targetDLENonce ${targetDLENonce}, updating target`); + targetDLENonce = current; + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`); } + // Если текущий nonce меньше целевого, выравниваем его if (current < targetDLENonce) { logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce alignment: ${current} -> ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); } else { logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned: ${current} = ${targetDLENonce}`); } + // 2) Выравниваем nonce если нужно (используем NonceManager) if (current < targetDLENonce) { - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce} (${targetDLENonce - current} transactions needed)`); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} aligning nonce from ${current} to ${targetDLENonce}`); - // Используем burn address для более надежных транзакций - const burnAddress = "0x000000000000000000000000000000000000dEaD"; - - while (current < targetDLENonce) { - const overrides = await getFeeOverrides(provider); - let gasLimit = 21000; // минимальный gas для обычной транзакции - let sent = false; - let lastErr = null; + try { + current = await nonceManager.alignNonceToTarget( + wallet.address, + rpcUrl, + chainId, + targetDLENonce, + wallet, + { gasLimit: 21000, maxRetries: 5 } + ); - for (let attempt = 0; attempt < 5 && !sent; attempt++) { - try { - const txReq = { - to: burnAddress, // отправляем на burn address вместо своего адреса - value: 0n, - nonce: current, - gasLimit, - ...overrides - }; - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} sending filler tx nonce=${current} attempt=${attempt + 1}/5`); - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} tx details: to=${burnAddress}, value=0, gasLimit=${gasLimit}`); - const { tx: txFill, receipt } = await sendTransactionWithRetry(wallet, txReq, { maxRetries: 3 }); - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx sent, hash=${txFill.hash}, waiting for confirmation...`); - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} confirmed, hash=${txFill.hash}`); - sent = true; - } catch (e) { - lastErr = e; - const errorMsg = e?.message || e; - logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} filler tx nonce=${current} attempt=${attempt + 1} failed: ${errorMsg}`); - - // Обработка специфических ошибок - if (String(errorMsg).toLowerCase().includes('intrinsic gas too low') && attempt < 4) { - gasLimit = Math.min(gasLimit * 2, 100000); // увеличиваем gas limit с ограничением - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} increased gas limit to ${gasLimit}`); - continue; - } - - if (String(errorMsg).toLowerCase().includes('nonce too low') && attempt < 4) { - // Сбрасываем кэш nonce и получаем актуальный - nonceManager.resetNonce(wallet.address, chainId); - const newNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce changed from ${current} to ${newNonce}`); - current = newNonce; - - // Если новый nonce больше целевого, обновляем targetDLENonce - if (current > targetDLENonce) { - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${current} > target nonce ${targetDLENonce}, updating target`); - targetDLENonce = current; - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`); - } - - continue; - } - - if (String(errorMsg).toLowerCase().includes('insufficient funds') && attempt < 4) { - logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} insufficient funds for nonce alignment`); - throw new Error(`Insufficient funds for nonce alignment on chainId=${Number(net.chainId)}. Please add more ETH to the wallet.`); - } - - if (String(errorMsg).toLowerCase().includes('network') && attempt < 4) { - logger.warn(`[MULTI_DBG] chainId=${Number(net.chainId)} network error, retrying in ${(attempt + 1) * 2} seconds...`); - await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 2000)); - continue; - } - - // Если это последняя попытка, выбрасываем ошибку - if (attempt === 4) { - throw new Error(`Failed to send filler transaction after 5 attempts: ${errorMsg}`); - } - } - } + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); - if (!sent) { - logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} failed to send filler tx for nonce=${current}`); - throw lastErr || new Error('filler tx failed'); - } + // Зарезервируем nonce в NonceManager + nonceManager.reserveNonce(wallet.address, chainId, targetDLENonce); + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`); - current++; + } catch (error) { + logger.error(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment failed: ${error.message}`); + throw error; } - - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce alignment completed, current nonce=${current}`); - - // Зарезервируем nonce в NonceManager - nonceManager.reserveNonce(wallet.address, chainId, targetDLENonce); - logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} ready for DLE deployment with nonce=${current}`); } else { logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce already aligned at ${current}`); } @@ -421,6 +339,10 @@ async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit if (params.logoURI && params.logoURI !== '') { try { logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} checking logoURI for existing contract`); + + // Ждем 2 секунды для стабильности соединения + await new Promise(resolve => setTimeout(resolve, 2000)); + const DLE = await hre.ethers.getContractFactory('contracts/DLE.sol:DLE'); const dleContract = DLE.attach(predictedAddress); @@ -454,6 +376,13 @@ async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} deploy attempt ${deployAttempts}/${maxDeployAttempts} with current nonce=${currentNonce} (target was ${targetDLENonce})`); + // Если текущий nonce больше целевого, обновляем targetDLENonce + if (currentNonce > targetDLENonce) { + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce ${currentNonce} > target nonce ${targetDLENonce}, updating target`); + targetDLENonce = currentNonce; + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} updated targetDLENonce to: ${targetDLENonce}`); + } + const txData = { data: dleInit, nonce: currentNonce, @@ -461,7 +390,8 @@ async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit ...feeOverrides }; - const result = await sendTransactionWithRetry(wallet, txData, { maxRetries: 3 }); + const rpcManager = new RPCConnectionManager(); + const result = await rpcManager.sendTransactionWithRetry(wallet, txData, { maxRetries: 3 }); tx = result.tx; // Отмечаем транзакцию как pending в NonceManager @@ -478,7 +408,8 @@ async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit if (String(errorMsg).toLowerCase().includes('nonce too low') && deployAttempts < maxDeployAttempts) { logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce race condition detected, retrying...`); - // Получаем актуальный nonce из сети + // Используем NonceManager для обновления nonce + nonceManager.resetNonce(wallet.address, chainId); const currentNonce = await nonceManager.getNonce(wallet.address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} current nonce: ${currentNonce}, target was: ${targetDLENonce}`); @@ -519,6 +450,11 @@ async function deployInNetwork(rpcUrl, pk, initCodeHash, targetDLENonce, dleInit if (params.logoURI && params.logoURI !== '') { try { logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI: ${params.logoURI}`); + + // Ждем 5 секунд, чтобы контракт получил подтверждения + logger.info(`[MULTI_DBG] chainId=${Number(net.chainId)} waiting 5 seconds for contract confirmations...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + const DLE = await hre.ethers.getContractFactory('contracts/DLE.sol:DLE'); const dleContract = DLE.attach(deployedAddress); diff --git a/backend/services/deploymentWebSocketService.js b/backend/services/deploymentWebSocketService.js index 4e93c39..9ddd0fb 100644 --- a/backend/services/deploymentWebSocketService.js +++ b/backend/services/deploymentWebSocketService.js @@ -234,16 +234,22 @@ class DeploymentWebSocketService { return; } - // Отправляем сообщение всем подключенным клиентам - this.wss.clients.forEach(ws => { - if (ws.readyState === WebSocket.OPEN) { - try { - ws.send(JSON.stringify(message)); - } catch (error) { - console.error('[DeploymentWS] Ошибка отправки сообщения:', error); + // Отправляем сообщение только клиентам, подписанным на этот DLE + if (this.clients.has(dleAddress)) { + const clients = this.clients.get(dleAddress); + console.log(`[DeploymentWS] Отправляем сообщение ${dleAddress} клиентам: ${clients.size}`); + clients.forEach(ws => { + if (ws.readyState === WebSocket.OPEN) { + try { + ws.send(JSON.stringify(message)); + } catch (error) { + console.error('[DeploymentWS] Ошибка отправки сообщения:', error); + } } - } - }); + }); + } else { + console.log(`[DeploymentWS] Нет клиентов для DLE: ${dleAddress}`); + } } /** diff --git a/backend/temp-module-args-hierarchicalVoting.js b/backend/temp-module-args-hierarchicalVoting.js new file mode 100644 index 0000000..a86aa52 --- /dev/null +++ b/backend/temp-module-args-hierarchicalVoting.js @@ -0,0 +1,3 @@ +module.exports = [ + "0x7DB7E55eA9050105cDeeC892A1B8D4Ea335b0BFD" +]; \ No newline at end of file diff --git a/backend/utils/deploymentUtils.js b/backend/utils/deploymentUtils.js index 4030ca0..40ad0e8 100644 --- a/backend/utils/deploymentUtils.js +++ b/backend/utils/deploymentUtils.js @@ -55,69 +55,7 @@ function createProviderAndWallet(rpcUrl, privateKey) { } } -/** - * Выравнивает nonce до целевого значения - * @param {Object} wallet - Кошелек ethers - * @param {Object} provider - Провайдер ethers - * @param {number} targetNonce - Целевой nonce - * @param {Object} options - Опции для настройки - * @returns {Promise} - Текущий nonce после выравнивания - */ -async function alignNonce(wallet, provider, targetNonce, options = {}) { - try { - // Используем nonceManager для получения актуального nonce - const network = await provider.getNetwork(); - const chainId = Number(network.chainId); - const rpcUrl = provider._getConnection?.()?.url || 'unknown'; - - let current = await nonceManager.getNonceFast(wallet.address, rpcUrl, chainId); - - if (current > targetNonce) { - throw new Error(`Current nonce ${current} > target nonce ${targetNonce}`); - } - - if (current < targetNonce) { - logger.info(`Выравнивание nonce: ${current} -> ${targetNonce} (${targetNonce - current} транзакций)`); - - const { burnAddress = '0x000000000000000000000000000000000000dEaD' } = options; - - for (let i = current; i < targetNonce; i++) { - const overrides = await getFeeOverrides(provider); - const gasLimit = 21000n; - - try { - const txFill = await wallet.sendTransaction({ - to: burnAddress, - value: 0, - gasLimit, - ...overrides - }); - - logger.info(`Filler tx sent, hash=${txFill.hash}, nonce=${i}`); - - await txFill.wait(); - logger.info(`Filler tx confirmed, hash=${txFill.hash}, nonce=${i}`); - - // Обновляем nonce в кэше - nonceManager.reserveNonce(wallet.address, chainId, i); - current = i + 1; - } catch (error) { - logger.error(`Filler tx failed for nonce=${i}:`, error); - throw error; - } - } - - logger.info(`Nonce alignment completed, current nonce=${current}`); - } else { - logger.info(`Nonce already aligned at ${current}`); - } - - return current; - } catch (error) { - logger.error('Ошибка при выравнивании nonce:', error); - throw error; - } -} +// alignNonce функция удалена - используем nonceManager.alignNonceToTarget() вместо этого /** * Получает информацию о сети @@ -177,50 +115,15 @@ async function createMultipleRPCConnections(rpcUrls, privateKey, options = {}) { return await rpcManager.createMultipleConnections(rpcUrls, privateKey, options); } -/** - * Выполняет транзакцию с retry логикой - * @param {Object} wallet - Кошелек - * @param {Object} txData - Данные транзакции - * @param {Object} options - Опции - * @returns {Promise} - Результат транзакции - */ -async function sendTransactionWithRetry(wallet, txData, options = {}) { - const rpcManager = new RPCConnectionManager(); - return await rpcManager.sendTransactionWithRetry(wallet, txData, options); -} +// sendTransactionWithRetry функция удалена - используем RPCConnectionManager напрямую -/** - * Получает nonce с retry логикой - * @param {Object} provider - Провайдер - * @param {string} address - Адрес - * @param {Object} options - Опции - * @returns {Promise} - Nonce - */ -async function getNonceWithRetry(provider, address, options = {}) { - // Используем быстрый метод по умолчанию - if (options.fast !== false) { - try { - const network = await provider.getNetwork(); - const chainId = Number(network.chainId); - const rpcUrl = provider._getConnection?.()?.url || 'unknown'; - return await nonceManager.getNonceFast(address, rpcUrl, chainId); - } catch (error) { - console.warn(`[deploymentUtils] Быстрый nonce failed, используем retry: ${error.message}`); - } - } - - // Fallback на retry метод - return await nonceManager.getNonceWithRetry(provider, address, options); -} +// getNonceWithRetry функция удалена - используем nonceManager.getNonceWithRetry() вместо этого module.exports = { getFeeOverrides, createProviderAndWallet, - alignNonce, getNetworkInfo, getBalance, createRPCConnection, - createMultipleRPCConnections, - sendTransactionWithRetry, - getNonceWithRetry + createMultipleRPCConnections }; diff --git a/backend/utils/nonceManager.js b/backend/utils/nonceManager.js index b0ef12e..e385a4c 100644 --- a/backend/utils/nonceManager.js +++ b/backend/utils/nonceManager.js @@ -21,13 +21,16 @@ class NonceManager { * @returns {Promise} - Актуальный nonce */ async getNonce(address, rpcUrl, chainId, options = {}) { - const { timeout = 10000, maxRetries = 3 } = options; // Увеличиваем таймаут и попытки + const { timeout = 15000, maxRetries = 5 } = options; // Увеличиваем таймаут и попытки const cacheKey = `${address}-${chainId}`; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - const provider = new ethers.JsonRpcProvider(rpcUrl); + const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, { + polling: false, // Отключаем polling для более быстрого получения nonce + staticNetwork: true + }); // Получаем nonce из сети с таймаутом const networkNonce = await Promise.race([ @@ -170,51 +173,7 @@ class NonceManager { }; } - /** - * Быстрое получение nonce без retry (для критичных по времени операций) - * @param {string} address - Адрес кошелька - * @param {string} rpcUrl - RPC URL сети - * @param {number} chainId - ID сети - * @returns {Promise} - Nonce - */ - async getNonceFast(address, rpcUrl, chainId) { - const cacheKey = `${address}-${chainId}`; - const cachedNonce = this.nonceCache.get(cacheKey); - - if (cachedNonce !== undefined) { - console.log(`[NonceManager] Быстрый nonce из кэша: ${cachedNonce} для ${address}:${chainId}`); - return cachedNonce; - } - - // Получаем RPC URLs из базы данных с fallback - const rpcUrls = await this.getRpcUrlsFromDatabase(chainId, rpcUrl); - - for (const currentRpc of rpcUrls) { - try { - console.log(`[NonceManager] Пробуем RPC: ${currentRpc}`); - const provider = new ethers.JsonRpcProvider(currentRpc); - - const networkNonce = await Promise.race([ - provider.getTransactionCount(address, 'pending'), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Fast nonce timeout')), 3000) - ) - ]); - - this.nonceCache.set(cacheKey, networkNonce); - console.log(`[NonceManager] ✅ Nonce получен: ${networkNonce} для ${address}:${chainId} с RPC: ${currentRpc}`); - return networkNonce; - } catch (error) { - console.warn(`[NonceManager] RPC failed: ${currentRpc} - ${error.message}`); - continue; - } - } - - // Если все RPC недоступны, возвращаем 0 - console.warn(`[NonceManager] Все RPC недоступны для ${address}:${chainId}, возвращаем 0`); - this.nonceCache.set(cacheKey, 0); - return 0; - } + // getNonceFast функция удалена - используем getNonce() вместо этого /** * Получить RPC URLs из базы данных с fallback @@ -330,6 +289,75 @@ class NonceManager { throw error; } } + + /** + * Выровнять nonce до целевого значения с помощью filler транзакций + * @param {string} address - Адрес кошелька + * @param {string} rpcUrl - RPC URL сети + * @param {number} chainId - ID сети + * @param {number} targetNonce - Целевой nonce + * @param {Object} wallet - Кошелек для отправки транзакций + * @param {Object} options - Опции (gasLimit, maxRetries) + * @returns {Promise} - Финальный nonce + */ + async alignNonceToTarget(address, rpcUrl, chainId, targetNonce, wallet, options = {}) { + const { gasLimit = 21000, maxRetries = 5 } = options; + const burnAddress = "0x000000000000000000000000000000000000dEaD"; + + // Получаем текущий nonce + let current = await this.getNonce(address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); + console.log(`[NonceManager] Aligning nonce ${current} -> ${targetNonce} for ${address}:${chainId}`); + + if (current >= targetNonce) { + console.log(`[NonceManager] Nonce already aligned: ${current} >= ${targetNonce}`); + return current; + } + + // Используем RPCConnectionManager для retry логики + const RPCConnectionManager = require('./rpcConnectionManager'); + const rpcManager = new RPCConnectionManager(); + + // Выравниваем nonce с помощью filler транзакций + while (current < targetNonce) { + console.log(`[NonceManager] Sending filler tx nonce=${current} for ${address}:${chainId}`); + + try { + const txData = { + to: burnAddress, + value: 0n, + nonce: current, + gasLimit, + }; + + const result = await rpcManager.sendTransactionWithRetry(wallet, txData, { maxRetries }); + console.log(`[NonceManager] Filler tx sent: ${result.tx.hash} for nonce=${current}`); + current++; + + } catch (e) { + const errorMsg = e?.message || e; + console.warn(`[NonceManager] Filler tx failed: ${errorMsg}`); + + // Обрабатываем ошибки nonce + if (String(errorMsg).toLowerCase().includes('nonce too low')) { + this.resetNonce(address, chainId); + const newNonce = await this.getNonce(address, rpcUrl, chainId, { timeout: 15000, maxRetries: 5 }); + console.log(`[NonceManager] Nonce changed from ${current} to ${newNonce}`); + current = newNonce; + + if (current >= targetNonce) { + console.log(`[NonceManager] Nonce alignment completed: ${current} >= ${targetNonce}`); + return current; + } + continue; + } + + throw new Error(`Failed to send filler tx for nonce=${current}: ${errorMsg}`); + } + } + + console.log(`[NonceManager] Nonce alignment completed: ${current} for ${address}:${chainId}`); + return current; + } } // Создаем глобальный экземпляр diff --git a/backend/utils/rpcConnectionManager.js b/backend/utils/rpcConnectionManager.js index 14fb050..74578e3 100644 --- a/backend/utils/rpcConnectionManager.js +++ b/backend/utils/rpcConnectionManager.js @@ -192,40 +192,7 @@ class RPCConnectionManager { ); } - /** - * Получает nonce с retry логикой - * @param {Object} provider - Провайдер - * @param {string} address - Адрес - * @param {Object} options - Опции - * @returns {Promise} - Nonce - */ - async getNonceWithRetry(provider, address, options = {}) { - const config = { ...this.retryConfig, ...options }; - - for (let attempt = 1; attempt <= config.maxRetries; attempt++) { - try { - const nonce = await Promise.race([ - provider.getTransactionCount(address, 'pending'), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Nonce timeout')), config.timeout) - ) - ]); - - logger.info(`[RPC_MANAGER] ✅ Nonce получен: ${nonce} (попытка ${attempt})`); - return nonce; - - } catch (error) { - logger.error(`[RPC_MANAGER] ❌ Nonce failed (попытка ${attempt}): ${error.message}`); - - if (attempt === config.maxRetries) { - throw new Error(`Не удалось получить nonce после ${config.maxRetries} попыток: ${error.message}`); - } - - const delay = Math.min(config.baseDelay * Math.pow(2, attempt - 1), config.maxDelay); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } + // getNonceWithRetry функция удалена - используем nonceManager.getNonceWithRetry() вместо этого /** * Очищает кэш соединений diff --git a/backend/wsHub.js b/backend/wsHub.js index f277c92..920df69 100644 --- a/backend/wsHub.js +++ b/backend/wsHub.js @@ -90,6 +90,18 @@ function initWSS(server) { // Отписка от деплоя deploymentWebSocketService.unsubscribeFromDeployment(ws, data.dleAddress); } + + // Обработка сообщений деплоя через deploymentWebSocketService + if (data.type === 'deployment_log' || + data.type === 'deployment_started' || + data.type === 'deployment_status' || + data.type === 'deployment_finished' || + data.type === 'deployment_update') { + // Эти сообщения обрабатываются в deploymentWebSocketService + // Просто логируем для отладки + console.log(`[WebSocket] Получено сообщение деплоя: ${data.type}`); + console.log(`[WebSocket] Данные:`, JSON.stringify(data, null, 2)); + } } catch (error) { // console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error); } diff --git a/frontend/src/components/deployment/DeploymentWizard.vue b/frontend/src/components/deployment/DeploymentWizard.vue index 4aad908..2650e9a 100644 --- a/frontend/src/components/deployment/DeploymentWizard.vue +++ b/frontend/src/components/deployment/DeploymentWizard.vue @@ -310,14 +310,31 @@ const startDeployment = async () => { // Автозапуск деплоя при появлении компонента onMounted(() => { + console.log('🔧 [DeploymentWizard] onMounted вызван'); + console.log('🔧 [DeploymentWizard] deploymentStatus.value:', deploymentStatus.value); + console.log('🔧 [DeploymentWizard] logs.value.length:', logs.value.length); + console.log('🔧 [DeploymentWizard] props:', props); + console.log('🔧 [DeploymentWizard] isDeploying.value:', isDeploying.value); + + // Добавляем тестовый лог сразу + addLog('🔧 [DeploymentWizard] Компонент смонтирован', 'info'); + if (deploymentStatus.value === 'not_started') { + console.log('🔧 [DeploymentWizard] Запускаем автоматический деплой'); addLog('🚀 Автоматически запускаем деплой...', 'info'); startDeployment(); + } else { + console.log('🔧 [DeploymentWizard] Деплой уже запущен, статус:', deploymentStatus.value); } }); // Следим за новыми логами и скроллим вниз -watch(logs, () => { +watch(logs, (newLogs, oldLogs) => { + console.log('📝 [DeploymentWizard] Логи изменились:', { + newLength: newLogs.length, + oldLength: oldLogs?.length || 0, + lastLog: newLogs[newLogs.length - 1] + }); scrollToBottom(); }, { deep: true }); diff --git a/frontend/src/composables/useDeploymentWebSocket.js b/frontend/src/composables/useDeploymentWebSocket.js index 22bf47b..e86df28 100644 --- a/frontend/src/composables/useDeploymentWebSocket.js +++ b/frontend/src/composables/useDeploymentWebSocket.js @@ -33,11 +33,13 @@ export function useDeploymentWebSocket() { // Добавить лог const addLog = (message, type = 'info') => { const timestamp = new Date().toLocaleTimeString(); + console.log('📝 [DeploymentWebSocket] Добавляем лог:', { timestamp, message, type }); logs.value.push({ timestamp, message, type }); + console.log('📝 [DeploymentWebSocket] Всего логов:', logs.value.length); }; // Очистить логи @@ -48,8 +50,52 @@ export function useDeploymentWebSocket() { // Обработчик WebSocket сообщений const handleDeploymentUpdate = (data) => { console.log('🔄 [DeploymentWebSocket] Получено обновление:', data); + + // Проверяем, что data существует и имеет type + if (!data || !data.type) { + console.warn('🔄 [DeploymentWebSocket] Получены некорректные данные:', data); + return; + } + console.log('🔄 [DeploymentWebSocket] Текущий deploymentId:', deploymentId.value); console.log('🔄 [DeploymentWebSocket] deploymentId из данных:', data.deploymentId); + console.log('🔄 [DeploymentWebSocket] Тип сообщения:', data.type); + console.log('🔄 [DeploymentWebSocket] Есть ли log:', !!data.log); + + // Обрабатываем deployment_update сообщения + if (data.type === 'deployment_update' && data.data) { + const updateData = data.data; + console.log('🔄 [DeploymentWebSocket] Обрабатываем deployment_update:', updateData); + console.log('🔄 [DeploymentWebSocket] updateData.deploymentId:', updateData.deploymentId); + console.log('🔄 [DeploymentWebSocket] Текущий deploymentId:', deploymentId.value); + + // Проверяем, что это наш деплой + if (updateData.deploymentId && updateData.deploymentId !== deploymentId.value) { + console.log('🔄 [DeploymentWebSocket] Игнорируем - не наш deploymentId'); + return; + } + + // Добавляем output как лог + if (updateData.output) { + console.log('🔄 [DeploymentWebSocket] Добавляем output как лог:', updateData.output); + addLog(updateData.output, 'info'); + } + + // Обновляем статус и прогресс + if (updateData.status) deploymentStatus.value = updateData.status; + if (updateData.progress !== undefined) progress.value = updateData.progress; + if (updateData.message) currentStage.value = updateData.message; + if (updateData.result) deploymentResult.value = updateData.result; + + return; + } + + // Всегда обрабатываем логи, даже если deploymentId не совпадает + if (data.type === 'deployment_log' && data.log) { + console.log('🔄 [DeploymentWebSocket] Добавляем лог:', data.log.message); + addLog(data.log.message, data.log.type || 'info'); + return; + } if (data.deploymentId !== deploymentId.value) { console.log('🔄 [DeploymentWebSocket] Игнорируем обновление - не наш deploymentId'); @@ -58,8 +104,31 @@ export function useDeploymentWebSocket() { switch (data.type) { case 'deployment_log': - if (data.log) { - addLog(data.log.message, data.log.type || 'info'); + // Уже обработано выше, пропускаем + break; + + case 'deployment_update': + // Обработка deployment_update сообщений + if (data.data) { + const updateData = data.data; + if (updateData.stage) currentStage.value = updateData.stage; + if (updateData.progress !== undefined) progress.value = updateData.progress; + if (updateData.status) deploymentStatus.value = updateData.status; + if (updateData.result) deploymentResult.value = updateData.result; + if (updateData.error) error.value = updateData.error; + if (updateData.output) { + addLog(updateData.output, 'info'); + } + if (updateData.message) { + addLog(updateData.message, 'info'); + } + if (updateData.status === 'completed') { + isDeploying.value = false; + addLog('🎉 Деплой успешно завершен!', 'success'); + } else if (updateData.status === 'failed') { + isDeploying.value = false; + addLog('💥 Деплой завершился с ошибкой!', 'error'); + } } break; @@ -98,15 +167,23 @@ export function useDeploymentWebSocket() { }; // Подключаемся к WebSocket сразу при инициализации + console.log('🔌 [DeploymentWebSocket] Инициализация WebSocket...'); + console.log('🔌 [DeploymentWebSocket] wsClient:', !!wsClient); + console.log('🔌 [DeploymentWebSocket] wsClient.subscribe:', typeof wsClient?.subscribe); + wsClient.connect(); if (wsClient && typeof wsClient.subscribe === 'function') { wsClient.subscribe('deployment_update', handleDeploymentUpdate); console.log('🔌 [DeploymentWebSocket] Подключились к WebSocket при инициализации'); + } else { + console.warn('🔌 [DeploymentWebSocket] WebSocket не доступен!'); } // Начать отслеживание деплоя const startDeploymentTracking = (id) => { console.log('🎯 [DeploymentWebSocket] Начинаем отслеживание деплоя:', id); + console.log('🎯 [DeploymentWebSocket] WebSocket подключен:', !!wsClient); + console.log('🎯 [DeploymentWebSocket] Обработчики deployment_update:', wsClient?.handlers?.deployment_update?.length || 0); deploymentId.value = id; deploymentStatus.value = 'in_progress'; @@ -114,6 +191,13 @@ export function useDeploymentWebSocket() { clearLogs(); // WebSocket уже подключен при инициализации + // Подписываемся на DLE адрес для получения логов деплоя + if (wsClient && typeof wsClient.subscribeToDeployment === 'function') { + // Используем временный DLE адрес для подписки на логи + const tempDleAddress = '0x0000000000000000000000000000000000000000'; + wsClient.subscribeToDeployment(tempDleAddress); + console.log('🎯 [DeploymentWebSocket] Подписались на DLE адрес:', tempDleAddress); + } addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info'); }; diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 1e35e4e..2f977f9 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -22,15 +22,31 @@ class WebSocketClient { this.reconnectDelay = 1000; this.listeners = new Map(); this.isConnected = false; + this.heartbeatInterval = null; + this.heartbeatTimeout = 30000; // 30 секунд } connect() { + // Если уже подключены, не создаем новое соединение + if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { + console.log('[WebSocket] Уже подключены, пропускаем'); + return; + } + + // Закрываем предыдущее соединение если есть + if (this.ws) { + this.ws.close(); + this.ws = null; + } + try { // В Docker окружении используем Vite прокси для WebSocket // Используем относительный путь, чтобы Vite прокси мог перенаправить запрос на backend const wsUrl = window.location.protocol === 'https:' ? 'wss://' + window.location.host + '/ws' : 'ws://' + window.location.host + '/ws'; + + console.log('[WebSocket] Подключаемся к:', wsUrl); this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { @@ -38,6 +54,9 @@ class WebSocketClient { this.isConnected = true; this.reconnectAttempts = 0; + // Запускаем heartbeat + this.startHeartbeat(); + // Уведомляем о подключении this.emit('connected'); }; @@ -46,6 +65,12 @@ class WebSocketClient { try { const data = JSON.parse(event.data); + // Обрабатываем pong сообщения для heartbeat + if (data.type === 'pong') { + console.log('[WebSocket] Получен pong, соединение активно'); + return; + } + // Логируем все deployment_update сообщения для отладки if (data.type === 'deployment_update') { console.log('[WebSocket] Получено deployment_update:', data); @@ -56,10 +81,14 @@ class WebSocketClient { if (this.listeners.has(data.type)) { console.log(`[WebSocket] Вызываем обработчики для типа: ${data.type}, количество: ${this.listeners.get(data.type).length}`); this.listeners.get(data.type).forEach(callback => { - callback(data.data); + // Передаем весь объект data, а не только data.data + callback(data); }); } else { - console.log(`[WebSocket] Нет обработчиков для типа: ${data.type}`); + // Не логируем сообщения о деплое, они обрабатываются в других компонентах + if (!data.type?.startsWith('deployment_') && !data.type?.startsWith('module_') && data.type !== 'subscribed' && data.type !== 'pong') { + console.log(`[WebSocket] Нет обработчиков для типа: ${data.type}`); + } } } catch (error) { console.error('[WebSocket] Ошибка парсинга сообщения:', error); @@ -86,17 +115,50 @@ class WebSocketClient { scheduleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; - console.log(`[WebSocket] Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); + console.log(`[WebSocket] Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts} через ${this.reconnectDelay * this.reconnectAttempts}мс`); setTimeout(() => { - this.connect(); + if (!this.isConnected) { + this.connect(); + } }, this.reconnectDelay * this.reconnectAttempts); } else { console.error('[WebSocket] Превышено максимальное количество попыток переподключения'); + // Сбрасываем счетчик через 30 секунд для повторных попыток + setTimeout(() => { + this.reconnectAttempts = 0; + console.log('[WebSocket] Сброс счетчика переподключений, можно попробовать снова'); + }, 30000); + } + } + + startHeartbeat() { + // Очищаем предыдущий интервал + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + + // Отправляем ping каждые 30 секунд + this.heartbeatInterval = setInterval(() => { + if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { + this.send('ping', { timestamp: Date.now() }); + } else { + console.log('[WebSocket] Соединение потеряно, переподключаемся...'); + this.isConnected = false; + this.scheduleReconnect(); + } + }, this.heartbeatTimeout); + } + + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; } } disconnect() { + this.stopHeartbeat(); if (this.ws) { this.ws.close(); this.ws = null; diff --git a/frontend/src/views/smartcontracts/ModulesView.vue b/frontend/src/views/smartcontracts/ModulesView.vue index 0f66252..a08d579 100644 --- a/frontend/src/views/smartcontracts/ModulesView.vue +++ b/frontend/src/views/smartcontracts/ModulesView.vue @@ -24,9 +24,9 @@

Модули DLE

-
- - {{ isModulesWSConnected ? 'Подключено' : 'Отключено' }} +
+ + {{ isWSConnected ? 'Подключено' : 'Отключено' }}

{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}

@@ -734,6 +734,7 @@ import { getDeploymentStatus } from '../../services/modulesService.js'; import api from '../../api/axios'; +import wsClient from '../../utils/websocket'; // Определяем props const props = defineProps({ @@ -783,14 +784,9 @@ const deploymentStep = ref(0); const progressPercentage = ref(0); const deploymentLogs = ref([]); -// WebSocket соединение -const deploymentWS = ref(null); +// WebSocket соединение (используем глобальный wsClient) const isWSConnected = ref(false); -// WebSocket для обновления модулей -const modulesWS = ref(null); -const isModulesWSConnected = ref(false); - // Debounce для предотвращения частых вызовов loadModules let loadModulesTimeout = null; @@ -1039,62 +1035,54 @@ function formatDate(dateString) { // Функции для работы с WebSocket function connectWebSocket() { - if (deploymentWS.value && deploymentWS.value.readyState === WebSocket.OPEN) { - return; + // Используем глобальный wsClient + wsClient.connect(); + isWSConnected.value = wsClient.isConnected; + + // Подписываемся на события модулей + wsClient.subscribe('subscribed', handleWebSocketMessage); + wsClient.subscribe('deployment_started', handleWebSocketMessage); + wsClient.subscribe('deployment_status', handleWebSocketMessage); + wsClient.subscribe('deployment_log', handleWebSocketMessage); + wsClient.subscribe('deployment_finished', handleWebSocketMessage); + wsClient.subscribe('error', handleWebSocketMessage); + wsClient.subscribe('modules_updated', handleWebSocketMessage); + wsClient.subscribe('module_verified', handleWebSocketMessage); + wsClient.subscribe('module_deployment_error', handleWebSocketMessage); + + // Подписываемся на деплой для текущего DLE + if (dleAddress.value) { + console.log('[ModulesView] Подписываемся на DLE:', dleAddress.value); + wsClient.ws.send(JSON.stringify({ + type: 'subscribe', + dleAddress: dleAddress.value + })); + } else { + console.warn('[ModulesView] dleAddress не найден для подписки'); } - - const wsUrl = `ws://localhost:8000/ws`; - deploymentWS.value = new WebSocket(wsUrl); - - deploymentWS.value.onopen = () => { - console.log('[ModulesView] WebSocket соединение установлено'); - isWSConnected.value = true; - - // Подписываемся на деплой для текущего DLE - if (dleAddress.value) { - deploymentWS.value.send(JSON.stringify({ - type: 'subscribe', - dleAddress: dleAddress.value - })); - } - }; - - deploymentWS.value.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleWebSocketMessage(data); - } catch (error) { - console.error('[ModulesView] Ошибка парсинга WebSocket сообщения:', error); - } - }; - - deploymentWS.value.onclose = () => { - console.log('[ModulesView] WebSocket соединение закрыто'); - isWSConnected.value = false; - - // Переподключаемся через 3 секунды - setTimeout(() => { - if (showDeploymentModal.value) { - connectWebSocket(); - } - }, 3000); - }; - - deploymentWS.value.onerror = (error) => { - console.error('[ModulesView] Ошибка WebSocket:', error); - isWSConnected.value = false; - }; } function handleWebSocketMessage(data) { console.log('[ModulesView] WebSocket сообщение:', data); + // Проверяем, что data существует и имеет type + if (!data || !data.type) { + console.warn('[ModulesView] Получены некорректные данные WebSocket:', data); + return; + } + + console.log('[ModulesView] Тип сообщения:', data.type); + console.log('[ModulesView] DLE адрес в сообщении:', data.dleAddress); + console.log('[ModulesView] Текущий DLE адрес:', dleAddress.value); + switch (data.type) { case 'subscribed': addLog('info', `Подписка на деплой активирована для DLE: ${data.dleAddress}`); break; case 'deployment_started': + console.log('[ModulesView] Показываем модалку деплоя'); + showDeploymentModal.value = true; deploymentStep.value = 1; progressPercentage.value = 10; moduleDeploymentStatus.value = 'starting'; @@ -1107,6 +1095,7 @@ function handleWebSocketMessage(data) { break; case 'deployment_log': + console.log('[ModulesView] Получен лог деплоя:', data.log); addLog(data.log.type, data.log.message); break; @@ -1131,6 +1120,31 @@ function handleWebSocketMessage(data) { case 'error': addLog('error', data.message); break; + + // Обработка сообщений модулей + case 'modules_updated': + // Автоматически обновляем список модулей + console.log('[ModulesView] Получено уведомление об обновлении модулей'); + loadModulesDebounced(); + break; + + case 'module_verified': + console.log('[ModulesView] Модуль верифицирован:', data); + addLog('success', `Модуль ${data.moduleType} верифицирован в сети ${data.network}`); + break; + + case 'module_deployment_error': + console.log('[ModulesView] Ошибка деплоя модуля:', data); + addLog('error', `Ошибка деплоя модуля ${data.moduleType}: ${data.error}`); + break; + + default: + console.log('[ModulesView] Неизвестный тип сообщения:', data.type, data); + // Для неизвестных типов просто логируем + if (data.log) { + addLog(data.log.type || 'info', data.log.message); + } + break; } } @@ -1150,103 +1164,22 @@ function updateDeploymentProgress(data) { } function disconnectWebSocket() { - if (deploymentWS.value) { - deploymentWS.value.close(); - deploymentWS.value = null; - isWSConnected.value = false; - } -} - -// Функции для работы с WebSocket модулей -function connectModulesWebSocket() { - if (modulesWS.value && modulesWS.value.readyState === WebSocket.OPEN) { - return; - } - - const wsUrl = `ws://localhost:8000/ws`; - modulesWS.value = new WebSocket(wsUrl); - - modulesWS.value.onopen = () => { - console.log('[ModulesView] WebSocket модулей соединение установлено'); - isModulesWSConnected.value = true; - - // Подписываемся на обновления модулей для текущего DLE - if (dleAddress.value) { - modulesWS.value.send(JSON.stringify({ - type: 'subscribe', - dleAddress: dleAddress.value - })); - } - }; - - modulesWS.value.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - handleModulesWebSocketMessage(data); - } catch (error) { - console.error('[ModulesView] Ошибка парсинга WebSocket сообщения модулей:', error); - } - }; - - modulesWS.value.onclose = () => { - console.log('[ModulesView] WebSocket модулей соединение закрыто'); - isModulesWSConnected.value = false; - - // Переподключаемся через 5 секунд - setTimeout(() => { - connectModulesWebSocket(); - }, 5000); - }; - - modulesWS.value.onerror = (error) => { - console.error('[ModulesView] Ошибка WebSocket модулей:', error); - isModulesWSConnected.value = false; - }; -} - -function handleModulesWebSocketMessage(data) { - console.log('[ModulesView] WebSocket модулей сообщение:', data); + // Отписываемся от всех событий модулей + wsClient.unsubscribe('subscribed', handleWebSocketMessage); + wsClient.unsubscribe('deployment_started', handleWebSocketMessage); + wsClient.unsubscribe('deployment_status', handleWebSocketMessage); + wsClient.unsubscribe('deployment_log', handleWebSocketMessage); + wsClient.unsubscribe('deployment_finished', handleWebSocketMessage); + wsClient.unsubscribe('error', handleWebSocketMessage); + wsClient.unsubscribe('modules_updated', handleWebSocketMessage); + wsClient.unsubscribe('module_verified', handleWebSocketMessage); + wsClient.unsubscribe('module_deployment_error', handleWebSocketMessage); - // Обрабатываем deployment_log в модульном WebSocket - if (data.type === 'deployment_log') { - addLog(data.log.type, data.log.message); - return; - } - - // Обрабатываем только сообщения, связанные с модулями, не с деплоем - if (data.type && data.type.startsWith('deployment_')) { - console.log('[ModulesView] Пропускаем сообщение о деплое в модульном WebSocket'); - return; - } - - switch (data.type) { - case 'modules_updated': - // Автоматически обновляем список модулей - console.log('[ModulesView] Получено уведомление об обновлении модулей'); - loadModulesDebounced(); - break; - - case 'module_verified': - // Обновляем статус верификации конкретного модуля - console.log(`[ModulesView] Модуль ${data.moduleType} верифицирован`); - loadModulesDebounced(); - break; - - case 'module_status_changed': - // Обновляем статус модуля - console.log(`[ModulesView] Статус модуля ${data.moduleType} изменен`); - loadModulesDebounced(); - break; - } + isWSConnected.value = false; } -function disconnectModulesWebSocket() { - if (modulesWS.value) { - modulesWS.value.close(); - modulesWS.value = null; - isModulesWSConnected.value = false; - } -} + + // Функции для работы с модальным окном function openDeploymentModal(moduleType) { @@ -1356,14 +1289,13 @@ onMounted(() => { loadDleData(); loadModules(); // Первоначальная загрузка без debounce - // Подключаемся к WebSocket для обновления модулей - connectModulesWebSocket(); + // Подключаемся к WebSocket (объединенное соединение) + connectWebSocket(); }); onUnmounted(() => { // Отключаем WebSocket при размонтировании компонента disconnectWebSocket(); - disconnectModulesWebSocket(); });