/** * Copyright (c) 2024-2026 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/VC-HB3-Accelerator */ import { ref, computed } from 'vue'; import { getProposals } from '@/services/proposalsService'; import { ethers } from 'ethers'; import { useProposalValidation } from './useProposalValidation'; import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract'; import api from '@/api/axios'; // Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted // Функция checkTokenBalance перенесена в useDleContract.js // Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом // Вспомогательная функция для получения имени цепочки function getChainName(chainId) { const chainNames = { 1: 'Ethereum', 11155111: 'Sepolia', 17000: 'Holesky', 421614: 'Arbitrum Sepolia', 84532: 'Base Sepolia', 137: 'Polygon', 56: 'BSC', 42161: 'Arbitrum' }; return chainNames[chainId] || `Chain ${chainId}`; } export function useProposals(dleAddress, isAuthenticated, userAddress) { const proposals = ref([]); const filteredProposals = ref([]); const isLoading = ref(false); const isVoting = ref(false); const isExecuting = ref(false); const isCancelling = ref(false); const statusFilter = ref(''); const searchQuery = ref(''); // Используем готовые функции из utils/dle-contract.js // Инициализируем валидацию const { validateProposals, filterRealProposals, filterActiveProposals, validationStats, isValidating } = useProposalValidation(); const loadProposals = async () => { try { isLoading.value = true; // Получаем информацию о всех DLE в разных цепочках console.log('[Proposals] Получаем информацию о всех DLE...'); const dleResponse = await api.get('/dle-v2'); if (!dleResponse.data.success) { console.error('Не удалось получить список DLE'); return; } const allDles = dleResponse.data.data || []; console.log(`[Proposals] Найдено DLE: ${allDles.length}`, allDles); // Группируем предложения по описанию для создания мульти-чейн представлений const proposalsByDescription = new Map(); // Загружаем предложения из каждой цепочки for (const dle of allDles) { if (!dle.networks || dle.networks.length === 0) continue; // КРИТИЧНО: Пропускаем DLE, если ни один из его адресов не совпадает с запрошенным dleAddress const hasMatchingAddress = dle.networks.some(network => network.address && network.address.toLowerCase() === (dleAddress.value || '').toLowerCase() ); if (dleAddress.value && !hasMatchingAddress) { console.log(`[Proposals] Пропускаем DLE ${dle.dleAddress || 'N/A'}: адрес ${dleAddress.value} не найден в networks`); continue; } for (const network of dle.networks) { try { console.log(`[Proposals] Загружаем предложения из цепочки ${network.chainId}, адрес: ${network.address}`); const response = await getProposals(network.address); console.log(`[Proposals] Ответ для цепочки ${network.chainId}:`, { success: response.success, proposalsCount: response.data?.proposals?.length || 0, hasError: !!response.error }); if (response.success) { // Бэкенд возвращает: { success: true, data: { proposals: [...], totalCount: ... } } const chainProposals = (response.data?.data?.proposals || response.data?.proposals || []); console.log(`[Proposals] Получено предложений для цепочки ${network.chainId}: ${chainProposals.length}`, chainProposals); // Добавляем информацию о цепочке к каждому предложению chainProposals.forEach(proposal => { proposal.chainId = network.chainId; proposal.contractAddress = network.address; proposal.networkName = getChainName(network.chainId); // Группируем предложения по описанию и инициатору const key = `${proposal.description}_${proposal.initiator}`; // Преобразуем время создания в число для сравнения // createdAt может быть ISO строкой или числом, timestamp - число в секундах const getTimestamp = (proposal) => { if (proposal.timestamp) return Number(proposal.timestamp); // timestamp в секундах if (proposal.createdAt) { if (typeof proposal.createdAt === 'string') { return Math.floor(new Date(proposal.createdAt).getTime() / 1000); // ISO строка -> секунды } return Number(proposal.createdAt); } return Math.floor(Date.now() / 1000); }; const proposalTimestamp = getTimestamp(proposal); if (!proposalsByDescription.has(key)) { proposalsByDescription.set(key, { id: proposal.id, // ID из первой найденной сети description: proposal.description, initiator: proposal.initiator, deadline: proposal.deadline, chains: new Map(), createdAt: proposalTimestamp, // Время создания в секундах uniqueId: key }); } // Добавляем информацию о цепочке const group = proposalsByDescription.get(key); // Если в этой сети уже есть предложение с таким ключом, выбираем более позднее (актуальное) const existingChainData = group.chains.get(network.chainId); // Унифицируем state - всегда число const normalizedState = typeof proposal.state === 'string' ? (proposal.state === 'active' ? 0 : NaN) : Number(proposal.state); // Убеждаемся, что id есть (fallback к proposalId из события, если id отсутствует) const proposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : (proposal.proposalId !== undefined ? Number(proposal.proposalId) : null); if (existingChainData) { // Если уже есть предложение в этой сети, сравниваем по времени создания const existingTime = getTimestamp(existingChainData); // Оставляем более позднее предложение (актуальное) if (proposalTimestamp > existingTime) { group.chains.set(network.chainId, { ...proposal, id: proposalId !== null ? proposalId : existingChainData.id, // Используем id с fallback chainId: network.chainId, contractAddress: network.address, networkName: getChainName(network.chainId), state: normalizedState, // Унифицированный state (число) timestamp: proposalTimestamp // Сохраняем числовой timestamp для удобства }); } // Иначе оставляем существующее } else { // Первое предложение в этой сети для данной группы group.chains.set(network.chainId, { ...proposal, id: proposalId !== null ? proposalId : 0, // Fallback к 0, если id отсутствует chainId: network.chainId, contractAddress: network.address, networkName: getChainName(network.chainId), state: normalizedState, // Унифицированный state (число) timestamp: proposalTimestamp // Сохраняем числовой timestamp для удобства }); } // Обновляем createdAt группы - минимальное время из всех предложений // После добавления нового предложения пересчитываем минимальное время const allChainTimes = Array.from(group.chains.values()) .map(c => getTimestamp(c)); group.createdAt = Math.min(...allChainTimes, proposalTimestamp); }); } } catch (error) { console.error(`Ошибка загрузки предложений из цепочки ${network.chainId}:`, error); console.error(`Детали ошибки для цепочки ${network.chainId}:`, { chainId: network.chainId, address: network.address, errorMessage: error.message, errorStack: error.stack }); } } } // Преобразуем в массив для отображения const rawProposals = Array.from(proposalsByDescription.values()).map(group => { const chainsArray = Array.from(group.chains.values()).map(chain => { // Унифицируем state для каждого chain - всегда число const normalizedState = typeof chain.state === 'string' ? (chain.state === 'active' ? 0 : NaN) : Number(chain.state); // Убеждаемся, что id есть (fallback) const chainId = chain.id !== undefined && chain.id !== null ? Number(chain.id) : (chain.proposalId !== undefined ? Number(chain.proposalId) : null); return { ...chain, id: chainId !== null ? chainId : 0, // Fallback к 0, если id отсутствует state: isNaN(normalizedState) ? 0 : normalizedState // Всегда число, fallback к 0 }; }); // Определяем общий state группы (число) - минимальный state из всех chains const groupState = chainsArray.length > 0 ? Math.min(...chainsArray.map(c => Number(c.state || 0))) : 0; return { ...group, chains: chainsArray, // Общий статус - число (0 = Active, 3 = Executed, 4 = Canceled, 5 = ReadyForExecution) state: groupState, // Общий executed - выполнен если выполнен во всех цепочках executed: chainsArray.length > 0 && chainsArray.every(c => c.executed), // Общий canceled - отменен если отменен в любой цепочке canceled: chainsArray.some(c => c.canceled) }; }); console.log(`[Proposals] Сгруппировано предложений: ${rawProposals.length}`); console.log(`[Proposals] Детали группировки:`, rawProposals); // Применяем валидацию предложений const validationResult = validateProposals(rawProposals); // Фильтруем только реальные предложения const realProposals = filterRealProposals(validationResult.validProposals); console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`); console.log(`[Proposals] Реальных предложений: ${realProposals.length}`); // Считаем активные только для статистики/логов (не выкидываем остальные из списка, // иначе фильтр "Все/Выполненные/Отмененные" в UI никогда не покажет эти статусы). const activeProposals = filterActiveProposals(realProposals); console.log(`[Proposals] Активных предложений: ${activeProposals.length}`); if (validationResult.errorCount > 0) { console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`); } // В UI должны попадать ВСЕ реальные предложения; дальше их фильтрует statusFilter/searchQuery proposals.value = realProposals; filterProposals(); } catch (error) { console.error('Ошибка загрузки предложений:', error); proposals.value = []; } finally { isLoading.value = false; } }; const filterProposals = () => { if (!proposals.value || proposals.value.length === 0) { filteredProposals.value = []; return; } let filtered = [...proposals.value]; if (statusFilter.value) { filtered = filtered.filter(proposal => { switch (statusFilter.value) { case 'active': return proposal.state === 0; // Pending case 'succeeded': return proposal.state === 1; // Succeeded case 'defeated': return proposal.state === 2; // Defeated case 'executed': return proposal.state === 3; // Executed case 'cancelled': return proposal.state === 4; // Canceled case 'ready': return proposal.state === 5; // ReadyForExecution default: return true; } }); } if (searchQuery.value) { const query = searchQuery.value.toLowerCase(); filtered = filtered.filter(proposal => proposal.description.toLowerCase().includes(query) || proposal.initiator.toLowerCase().includes(query) || proposal.uniqueId.toLowerCase().includes(query) ); } filteredProposals.value = filtered; }; const voteOnProposal = async (proposalId, support) => { try { console.log('🚀 [VOTE] Начинаем голосование через DLE контракт:', { proposalId, support, dleAddress: dleAddress.value, userAddress: userAddress.value }); isVoting.value = true; // Проверяем наличие MetaMask if (!window.ethereum) { throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.'); } // Проверяем состояние предложения console.log('🔍 [DEBUG] Проверяем состояние предложения...'); const proposal = proposals.value.find(p => p.id === proposalId); if (!proposal) { throw new Error('Предложение не найдено'); } // КРИТИЧЕСКИ ВАЖНО: Если предложение мультичейн, используем voteOnMultichainProposal if (proposal.chains && proposal.chains.length > 1) { console.log('🌐 [VOTE] Обнаружено мультичейн предложение, используем voteOnMultichainProposal'); return await voteOnMultichainProposal(proposal, support); } console.log('📊 [DEBUG] Данные предложения:', { id: proposal.id, state: proposal.state, deadline: proposal.deadline, forVotes: proposal.forVotes, againstVotes: proposal.againstVotes, executed: proposal.executed, canceled: proposal.canceled }); // Проверяем, что предложение активно (Pending) if (proposal.state !== 0) { const statusText = getProposalStatusText(proposal.state); throw new Error(`Предложение не активно (статус: ${statusText}). Голосование возможно только для активных предложений.`); } // Проверяем, что предложение не выполнено и не отменено if (proposal.executed) { throw new Error('Предложение уже выполнено. Голосование невозможно.'); } if (proposal.canceled) { throw new Error('Предложение отменено. Голосование невозможно.'); } // Проверяем deadline const currentTime = Math.floor(Date.now() / 1000); if (proposal.deadline && currentTime > proposal.deadline) { throw new Error('Время голосования истекло. Голосование невозможно.'); } // Проверяем баланс токенов пользователя console.log('💰 [DEBUG] Проверяем баланс токенов...'); try { const balanceCheck = await checkTokenBalance(dleAddress.value, userAddress.value); console.log('💰 [DEBUG] Баланс токенов:', balanceCheck); if (!balanceCheck.hasTokens) { throw new Error('У вас нет токенов для голосования. Необходимо иметь токены DLE для участия в голосовании.'); } } catch (balanceError) { console.warn('⚠️ [DEBUG] Ошибка проверки баланса (продолжаем):', balanceError.message); // Не останавливаем голосование, если не удалось проверить баланс } // Проверяем сеть кошелька console.log('🌐 [DEBUG] Проверяем сеть кошелька...'); try { const chainId = await window.ethereum.request({ method: 'eth_chainId' }); console.log('🌐 [DEBUG] Текущая сеть:', chainId); console.log('🌐 [DEBUG] Сеть предложения:', proposal.chainId); if (chainId !== proposal.chainId) { throw new Error(`Неправильная сеть! Текущая сеть: ${chainId}, требуется: ${proposal.chainId}`); } } catch (networkError) { console.warn('⚠️ [DEBUG] Ошибка проверки сети (продолжаем):', networkError.message); } // Голосуем через готовую функцию из utils/dle-contract.js console.log('🗳️ Отправляем голосование через смарт-контракт...'); const result = await voteForProposal(dleAddress.value, proposalId, support); console.log('✅ Голосование успешно отправлено:', result.txHash); alert(`Голосование успешно отправлено! Хеш транзакции: ${result.txHash}`); // Принудительно обновляем данные предложения console.log('🔄 [VOTE] Обновляем данные после голосования...'); await loadProposals(); // Дополнительная задержка для подтверждения в блокчейне setTimeout(async () => { console.log('🔄 [VOTE] Повторное обновление через 3 секунды...'); await loadProposals(); }, 3000); } catch (error) { console.error('❌ Ошибка голосования:', error); // Улучшенная обработка ошибок let errorMessage = error.message; if (error.message.includes('execution reverted')) { if (error.data === '0xe7005635') { errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' + '• Вы уже голосовали за это предложение\n' + '• У вас нет токенов для голосования\n' + '• Предложение не активно\n' + '• Время голосования истекло'; } else if (error.data === '0xc7567e07') { errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' + '• Вы уже голосовали за это предложение\n' + '• У вас нет токенов для голосования\n' + '• Предложение не активно\n' + '• Время голосования истекло\n' + '• Неправильная сеть для голосования'; } else { errorMessage = `Транзакция отклонена смарт-контрактом (код: ${error.data}). Проверьте условия голосования.`; } } else if (error.message.includes('user rejected')) { errorMessage = 'Транзакция отклонена пользователем'; } else if (error.message.includes('insufficient funds')) { errorMessage = 'Недостаточно средств для оплаты газа'; } alert('Ошибка при голосовании: ' + errorMessage); } finally { isVoting.value = false; } }; const executeProposal = async (proposalId) => { try { console.log('⚡ [EXECUTE] Исполняем предложение через DLE контракт:', { proposalId, dleAddress: dleAddress.value }); isExecuting.value = true; // Проверяем состояние предложения перед выполнением console.log('🔍 [DEBUG] Проверяем состояние предложения для выполнения...'); const proposal = proposals.value.find(p => p.id === proposalId); if (!proposal) { throw new Error('Предложение не найдено'); } // КРИТИЧЕСКИ ВАЖНО: Если предложение мультичейн, используем executeMultichainProposal if (proposal.chains && proposal.chains.length > 1) { console.log('🌐 [EXECUTE] Обнаружено мультичейн предложение, используем executeMultichainProposal'); return await executeMultichainProposal(proposal); } console.log('📊 [DEBUG] Данные предложения для выполнения:', { id: proposal.id, state: proposal.state, executed: proposal.executed, canceled: proposal.canceled, quorumReached: proposal.quorumReached }); // Проверяем, что предложение можно выполнить if (proposal.executed) { throw new Error('Предложение уже выполнено. Повторное выполнение невозможно.'); } if (proposal.canceled) { throw new Error('Предложение отменено. Выполнение невозможно.'); } // Проверяем, что предложение готово к выполнению if (proposal.state !== 5) { const statusText = getProposalStatusText(proposal.state); throw new Error(`Предложение не готово к выполнению (статус: ${statusText}). Выполнение возможно только для предложений в статусе "Готово к выполнению".`); } // Исполняем предложение через готовую функцию из utils/dle-contract.js const result = await executeProposalUtil(dleAddress.value, proposalId); console.log('✅ Предложение успешно исполнено:', result.txHash); alert(`Предложение успешно исполнено! Хеш транзакции: ${result.txHash}`); // Принудительно обновляем состояние предложения в UI updateProposalState(proposalId, { executed: true, state: 1, // Выполнено canceled: false }); await loadProposals(); // Перезагружаем данные } catch (error) { console.error('❌ Ошибка выполнения предложения:', error); // Улучшенная обработка ошибок let errorMessage = error.message; if (error.message.includes('execution reverted')) { errorMessage = 'Выполнение отклонено смарт-контрактом. Возможные причины:\n' + '• Предложение уже выполнено\n' + '• Предложение отменено\n' + '• Кворум не достигнут\n' + '• Предложение не активно'; } else if (error.message.includes('user rejected')) { errorMessage = 'Транзакция отклонена пользователем'; } else if (error.message.includes('insufficient funds')) { errorMessage = 'Недостаточно средств для оплаты газа'; } alert('Ошибка при исполнении предложения: ' + errorMessage); } finally { isExecuting.value = false; } }; const cancelProposal = async (proposalId, reason = 'Отменено пользователем') => { try { console.log('❌ [CANCEL] Отменяем предложение через DLE контракт:', { proposalId, reason, dleAddress: dleAddress.value }); isCancelling.value = true; // Проверяем состояние предложения перед отменой console.log('🔍 [DEBUG] Проверяем состояние предложения для отмены...'); const proposal = proposals.value.find(p => p.id === proposalId); if (!proposal) { throw new Error('Предложение не найдено'); } console.log('📊 [DEBUG] Данные предложения для отмены:', { id: proposal.id, state: proposal.state, executed: proposal.executed, canceled: proposal.canceled, deadline: proposal.deadline, chains: proposal.chains?.length || 0 }); // Проверяем, что предложение можно отменить if (proposal.executed) { throw new Error('Предложение уже выполнено. Отмена невозможна.'); } if (proposal.canceled) { throw new Error('Предложение уже отменено. Повторная отмена невозможна.'); } // Проверяем, что пользователь является инициатором if (proposal.initiator?.toLowerCase() !== userAddress.value?.toLowerCase()) { throw new Error('Только инициатор предложения может его отменить.'); } // Проверяем deadline (нужен запас 15 минут) const currentTime = Math.floor(Date.now() / 1000); if (proposal.deadline) { const timeRemaining = proposal.deadline - currentTime; if (timeRemaining <= 900) { // 15 минут запас throw new Error('Время для отмены истекло. Отмена возможна только за 15 минут до окончания голосования.'); } } // КРИТИЧЕСКИ ВАЖНО: Мультичейн отмена - последовательно во всех активных сетях if (proposal.chains && proposal.chains.length > 0) { // Фильтруем только активные цепочки (можно отменить) const activeChains = proposal.chains.filter(chain => canCancel(chain) && !chain.canceled && !chain.executed ); if (activeChains.length === 0) { throw new Error('Не найдено ни одной активной цепочки для отмены'); } console.log(`🚀 [MULTI-CANCEL] Начинаем отмену в ${activeChains.length} цепочках последовательно...`); const { switchToVotingNetwork } = await import('@/utils/dle-contract'); const results = []; // КРИТИЧЕСКИ ВАЖНО: Отменяем ПОСЛЕДОВАТЕЛЬНО, а не параллельно! // MetaMask может работать только с одной сетью одновременно for (let index = 0; index < activeChains.length; index++) { const chain = activeChains[index]; console.log(`📝 [${index + 1}/${activeChains.length}] Отмена в цепочке ${chain.networkName} (${chain.chainId})`); try { // Переключаемся на нужную сеть console.log(`🔄 [${index + 1}/${activeChains.length}] Переключаемся на сеть ${chain.chainId}...`); const switched = await switchToVotingNetwork(chain.chainId); if (!switched) { throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`); } // Задержка после переключения сети await new Promise(resolve => setTimeout(resolve, 1000)); const contractAddress = chain.contractAddress || chain.address || dleAddress.value; // Используем ID предложения из конкретной цепочки (с fallback) let chainProposalId = chain.id !== undefined && chain.id !== null ? Number(chain.id) : (chain.proposalId !== undefined ? Number(chain.proposalId) : null); // Fallback к proposalId, если chain.id отсутствует if (chainProposalId === null || isNaN(chainProposalId)) { chainProposalId = proposalId !== undefined && proposalId !== null ? Number(proposalId) : null; } if (chainProposalId === null || isNaN(chainProposalId)) { throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposalId=${proposalId}`); } chainProposalId = Number(chainProposalId); // Убеждаемся, что это число console.log(`🔍 [${index + 1}/${activeChains.length}] Используем ID предложения: ${chainProposalId} для отмены в цепочке ${chain.chainId}`); // Отменяем предложение console.log(`❌ [${index + 1}/${activeChains.length}] Отправляем отмену...`); const result = await cancelProposalUtil(contractAddress, chainProposalId, reason); console.log(`✅ [${index + 1}/${activeChains.length}] Предложение успешно отменено в ${chain.networkName}:`, result.txHash); // Задержка после подтверждения транзакции (для Base Sepolia больше) const delay = chain.chainId === 84532 ? 5000 : 3000; await new Promise(resolve => setTimeout(resolve, delay)); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: true, txHash: result.txHash }); } catch (error) { console.error(`❌ [${index + 1}/${activeChains.length}] Ошибка отмены в ${chain.networkName}:`, error); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: false, error: error.message }); // Продолжаем отменять в других цепочках даже при ошибке } } // Подводим итоги const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); console.log(`📊 [MULTI-CANCEL] Отмена завершена: успешно в ${successful.length} из ${activeChains.length} цепочек`); if (successful.length > 0) { alert(`Предложение отменено в ${successful.length} из ${activeChains.length} цепочек!\n${failed.length > 0 ? `Ошибки в ${failed.length} цепочках.` : ''}`); } else { throw new Error('Не удалось отменить предложение ни в одной цепочке'); } } else { // Одиночное предложение (без мультичейн) const result = await cancelProposalUtil(dleAddress.value, proposalId, reason); console.log('✅ Предложение успешно отменено:', result.txHash); alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`); } // Принудительно обновляем состояние предложения в UI updateProposalState(proposalId, { canceled: true, state: 4, // Canceled executed: false }); await loadProposals(); // Перезагружаем данные } catch (error) { console.error('❌ Ошибка отмены предложения:', error); // Улучшенная обработка ошибок let errorMessage = error.message; if (error.message.includes('execution reverted')) { errorMessage = 'Отмена отклонена смарт-контрактом. Возможные причины:\n' + '• Предложение уже отменено\n' + '• Предложение уже выполнено\n' + '• Предложение не активно\n' + '• Недостаточно прав для отмены'; } else if (error.message.includes('user rejected')) { errorMessage = 'Транзакция отклонена пользователем'; } else if (error.message.includes('insufficient funds')) { errorMessage = 'Недостаточно средств для оплаты газа'; } alert('Ошибка при отмене предложения: ' + errorMessage); } finally { isCancelling.value = false; } }; const getProposalStatusClass = (state) => { switch (state) { case 0: return 'status-active'; // Pending case 1: return 'status-succeeded'; // Succeeded case 2: return 'status-defeated'; // Defeated case 3: return 'status-executed'; // Executed case 4: return 'status-cancelled'; // Canceled case 5: return 'status-ready'; // ReadyForExecution default: return 'status-active'; } }; const getProposalStatusText = (state) => { switch (state) { case 0: return 'Активное'; case 1: return 'Успешное'; case 2: return 'Отклоненное'; case 3: return 'Выполнено'; case 4: return 'Отменено'; case 5: return 'Готово к выполнению'; default: return 'Неизвестно'; } }; const getQuorumPercentage = (proposal) => { // Получаем реальные данные из предложения const forVotes = Number(proposal.forVotes || 0); const againstVotes = Number(proposal.againstVotes || 0); const totalVotes = forVotes + againstVotes; // Используем реальный totalSupply из предложения или fallback const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE console.log(`📊 [QUORUM] Предложение ${proposal.id}:`, { forVotes: forVotes, againstVotes: againstVotes, totalVotes: totalVotes, totalSupply: totalSupply, forVotesFormatted: `${(forVotes / 1e+18).toFixed(2)} DLE`, againstVotesFormatted: `${(againstVotes / 1e+18).toFixed(2)} DLE`, totalVotesFormatted: `${(totalVotes / 1e+18).toFixed(2)} DLE`, totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE` }); const percentage = totalSupply > 0 ? (totalVotes / totalSupply) * 100 : 0; return percentage.toFixed(2); }; const getRequiredQuorumPercentage = (proposal) => { // Получаем требуемый кворум из предложения const requiredQuorum = Number(proposal.quorumRequired || 0); // Используем реальный totalSupply из предложения или fallback const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE console.log(`📊 [REQUIRED QUORUM] Предложение ${proposal.id}:`, { requiredQuorum: requiredQuorum, totalSupply: totalSupply, requiredQuorumFormatted: `${(requiredQuorum / 1e+18).toFixed(2)} DLE`, totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE` }); const percentage = totalSupply > 0 ? (requiredQuorum / totalSupply) * 100 : 0; return percentage.toFixed(2); }; const canVote = (proposal) => { // Для мультичейн предложений используем canVoteMultichain if (proposal.chains && proposal.chains.length > 1) { return canVoteMultichain(proposal); } // Унифицируем state - всегда число const state = typeof proposal.state === 'string' ? (proposal.state === 'active' ? 0 : NaN) : Number(proposal.state); return state === 0; // Pending - только активные предложения }; const canExecute = (proposal) => { // Унифицируем state - всегда число const state = typeof proposal.state === 'string' ? (proposal.state === 'active' ? 0 : NaN) : Number(proposal.state); return state === 5; // ReadyForExecution - готово к выполнению }; const canCancel = (proposal) => { // Унифицируем state - всегда число const state = typeof proposal.state === 'string' ? (proposal.state === 'active' ? 0 : NaN) : Number(proposal.state); // Можно отменить только активные предложения (Pending) return state === 0 && !proposal.executed && !proposal.canceled; }; // Принудительное обновление состояния предложения в UI const updateProposalState = (proposalId, updates) => { const proposal = proposals.value.find(p => p.id === proposalId); if (proposal) { Object.assign(proposal, updates); console.log(`🔄 [UI] Обновлено состояние предложения ${proposalId}:`, updates); // Принудительно обновляем фильтрацию filterProposals(); } }; // Мульти-чейн функции const voteOnMultichainProposal = async (proposal, support) => { try { isVoting.value = true; // Фильтруем только активные цепочки (state === 0 или 'active', не выполнены, не отменены) const activeChains = proposal.chains.filter(chain => canVote(chain)); if (activeChains.length === 0) { throw new Error('Не найдено ни одной активной цепочки для голосования'); } console.log(`🌐 [MULTI-VOTE] Начинаем голосование в ${activeChains.length} цепочках последовательно...`); const { switchToVotingNetwork } = await import('@/utils/dle-contract'); const results = []; // КРИТИЧЕСКИ ВАЖНО: Голосуем ПОСЛЕДОВАТЕЛЬНО, а не параллельно! // MetaMask может работать только с одной сетью одновременно for (let index = 0; index < activeChains.length; index++) { const chain = activeChains[index]; console.log(`📝 [${index + 1}/${activeChains.length}] Голосование в цепочке ${chain.networkName} (${chain.chainId})`); try { // Переключаемся на нужную сеть console.log(`🔄 [${index + 1}/${activeChains.length}] Переключаемся на сеть ${chain.chainId}...`); const switched = await switchToVotingNetwork(chain.chainId); if (!switched) { throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`); } // Задержка после переключения сети await new Promise(resolve => setTimeout(resolve, 1000)); const contractAddress = chain.contractAddress || chain.address || dleAddress.value; // Используем ID предложения из конкретной цепочки (с fallback) let chainProposalId = chain.id !== undefined && chain.id !== null ? Number(chain.id) : (chain.proposalId !== undefined ? Number(chain.proposalId) : null); // Fallback к proposal.id, если chain.id отсутствует if (chainProposalId === null || isNaN(chainProposalId)) { chainProposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : null; } if (chainProposalId === null || isNaN(chainProposalId)) { throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposal.id=${proposal.id}`); } chainProposalId = Number(chainProposalId); // Убеждаемся, что это число console.log(`🔍 [${index + 1}/${activeChains.length}] Используем ID предложения: ${chainProposalId} для голосования в цепочке ${chain.chainId}`); // Проверяем баланс токенов в каждой сети (балансы могут отличаться в разных сетях) console.log(`💰 [${index + 1}/${activeChains.length}] Проверяем баланс токенов в ${chain.networkName}...`); try { const balanceCheck = await checkTokenBalance(contractAddress, userAddress.value); console.log(`💰 [${index + 1}/${activeChains.length}] Баланс токенов в ${chain.networkName}:`, balanceCheck); if (!balanceCheck.hasTokens) { console.warn(`⚠️ [${index + 1}/${activeChains.length}] Нет токенов в ${chain.networkName}, пропускаем голосование в этой сети`); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: false, error: `Нет токенов DLE в сети ${chain.networkName} для голосования` }); // Продолжаем с следующей сетью continue; } } catch (balanceError) { console.warn(`⚠️ [${index + 1}/${activeChains.length}] Ошибка проверки баланса в ${chain.networkName} (продолжаем):`, balanceError.message); // При ошибке проверки баланса продолжаем попытку голосования // Контракт сам проверит баланс и вернет ошибку, если токенов нет } // Голосуем console.log(`🗳️ [${index + 1}/${activeChains.length}] Отправляем голосование для proposalId=${chainProposalId} в ${chain.networkName}...`); const result = await voteForProposal(contractAddress, chainProposalId, support); console.log(`✅ [${index + 1}/${activeChains.length}] Голосование успешно в ${chain.networkName}:`, result.txHash); // Задержка после подтверждения транзакции (для Base Sepolia больше) const delay = chain.chainId === 84532 ? 5000 : 3000; await new Promise(resolve => setTimeout(resolve, delay)); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: true, txHash: result.txHash }); } catch (error) { console.error(`❌ [${index + 1}/${activeChains.length}] Ошибка голосования в ${chain.networkName}:`, error); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: false, error: error.message }); // Продолжаем голосовать в других цепочках даже при ошибке } } // Подводим итоги const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); console.log(`📊 [MULTI-VOTE] Голосование завершено: успешно в ${successful.length} из ${activeChains.length} цепочек`); // Перезагружаем предложения await loadProposals(); } catch (error) { console.error('[MULTI-VOTE] Критическая ошибка:', error); throw error; } finally { isVoting.value = false; } }; const executeMultichainProposal = async (proposal) => { try { isExecuting.value = true; // Фильтруем только готовые к выполнению цепочки const readyChains = proposal.chains.filter(chain => canExecute(chain)); if (readyChains.length === 0) { throw new Error('Нет цепочек, готовых к выполнению'); } console.log(`🚀 [MULTI-EXECUTE] Начинаем исполнение в ${readyChains.length} цепочках последовательно...`); const { switchToVotingNetwork } = await import('@/utils/dle-contract'); const results = []; // КРИТИЧЕСКИ ВАЖНО: Исполняем ПОСЛЕДОВАТЕЛЬНО, а не параллельно! // MetaMask может работать только с одной сетью одновременно for (let index = 0; index < readyChains.length; index++) { const chain = readyChains[index]; console.log(`📝 [${index + 1}/${readyChains.length}] Выполнение в цепочке ${chain.networkName} (${chain.chainId})`); try { // Переключаемся на нужную сеть console.log(`🔄 [${index + 1}/${readyChains.length}] Переключаемся на сеть ${chain.chainId}...`); const switched = await switchToVotingNetwork(chain.chainId); if (!switched) { throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`); } // Задержка после переключения сети await new Promise(resolve => setTimeout(resolve, 1000)); const contractAddress = chain.contractAddress || chain.address || dleAddress.value; // Используем ID предложения из конкретной цепочки (с fallback) let chainProposalId = chain.id !== undefined && chain.id !== null ? Number(chain.id) : (chain.proposalId !== undefined ? Number(chain.proposalId) : null); // Fallback к proposal.id, если chain.id отсутствует if (chainProposalId === null || isNaN(chainProposalId)) { chainProposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : null; } if (chainProposalId === null || isNaN(chainProposalId)) { throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposal.id=${proposal.id}`); } chainProposalId = Number(chainProposalId); // Убеждаемся, что это число console.log(`🔍 [${index + 1}/${readyChains.length}] Используем ID предложения: ${chainProposalId} для выполнения в цепочке ${chain.chainId}`); // Выполняем предложение console.log(`⚡ [${index + 1}/${readyChains.length}] Отправляем выполнение...`); const result = await executeProposalUtil(contractAddress, chainProposalId); console.log(`✅ [${index + 1}/${readyChains.length}] Предложение успешно выполнено в ${chain.networkName}:`, result.txHash); // Задержка после подтверждения транзакции (для Base Sepolia больше) const delay = chain.chainId === 84532 ? 5000 : 3000; await new Promise(resolve => setTimeout(resolve, delay)); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: true, txHash: result.txHash }); } catch (error) { console.error(`❌ [${index + 1}/${readyChains.length}] Ошибка выполнения в ${chain.networkName}:`, error); results.push({ chainId: chain.chainId, networkName: chain.networkName, success: false, error: error.message }); // Продолжаем выполнять в других цепочках даже при ошибке } } // Подводим итоги const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); console.log(`📊 [MULTI-EXECUTE] Выполнение завершено: успешно в ${successful.length} из ${readyChains.length} цепочек`); // Перезагружаем предложения await loadProposals(); } catch (error) { console.error('[MULTI-EXECUTE] Критическая ошибка:', error); throw error; } finally { isExecuting.value = false; } }; const canVoteMultichain = (proposal) => { // Можно голосовать если есть хотя бы одна активная цепочка return proposal.chains.some(chain => canVote(chain)); }; const canExecuteMultichain = (proposal) => { // Можно исполнить только если кворум достигнут во ВСЕХ цепочках return proposal.chains.every(chain => canExecute(chain)); }; const getChainStatusClass = (chain) => { if (chain.executed) return 'executed'; if (chain.state === 'active') return 'active'; if (chain.deadline && chain.deadline < Date.now() / 1000) return 'expired'; return 'inactive'; }; const getChainStatusText = (chain) => { if (chain.executed) return 'Исполнено'; if (chain.state === 'active') return 'Активно'; if (chain.deadline && chain.deadline < Date.now() / 1000) return 'Истекло'; return 'Неактивно'; }; return { // ... существующие поля proposals, filteredProposals, isLoading, isVoting, isExecuting, isCancelling, statusFilter, searchQuery, loadProposals, filterProposals, voteOnProposal, voteOnMultichainProposal, executeProposal, executeMultichainProposal, cancelProposal, getProposalStatusClass, getProposalStatusText, getQuorumPercentage, getRequiredQuorumPercentage, canVote, canVoteMultichain, canExecute, canExecuteMultichain, canCancel, getChainStatusClass, getChainStatusText, updateProposalState, // Валидация validationStats, isValidating }; }