Files
DLE/frontend/src/composables/useProposals.js
2025-10-30 22:41:04 +03:00

546 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/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';
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
// Функция checkTokenBalance перенесена в useDleContract.js
// Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом
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 () => {
if (!dleAddress.value) {
console.warn('Адрес DLE не найден');
return;
}
try {
isLoading.value = true;
const response = await getProposals(dleAddress.value);
if (response.success) {
const rawProposals = response.data.proposals || [];
console.log(`[Proposals] Загружено предложений: ${rawProposals.length}`);
console.log(`[Proposals] Полные данные из блокчейна:`, rawProposals);
// Детальная информация о каждом предложении
rawProposals.forEach((proposal, index) => {
console.log(`[Proposals] Предложение ${index}:`, {
id: proposal.id,
description: proposal.description,
state: proposal.state,
forVotes: proposal.forVotes,
againstVotes: proposal.againstVotes,
quorumRequired: proposal.quorumRequired,
quorumReached: proposal.quorumReached,
executed: proposal.executed,
canceled: proposal.canceled,
initiator: proposal.initiator,
chainId: proposal.chainId,
transactionHash: proposal.transactionHash
});
});
// Применяем валидацию предложений
const validationResult = validateProposals(rawProposals);
// Фильтруем только реальные предложения
const realProposals = filterRealProposals(validationResult.validProposals);
// Фильтруем только активные предложения (исключаем выполненные и отмененные)
const activeProposals = filterActiveProposals(realProposals);
console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`);
console.log(`[Proposals] Реальных предложений: ${realProposals.length}`);
console.log(`[Proposals] Активных предложений: ${activeProposals.length}`);
if (validationResult.errorCount > 0) {
console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`);
}
proposals.value = activeProposals;
filterProposals();
}
} catch (error) {
console.error('Ошибка загрузки предложений:', error);
} 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('Предложение не найдено');
}
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('Предложение не найдено');
}
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
});
// Проверяем, что предложение можно отменить
if (proposal.executed) {
throw new Error('Предложение уже выполнено. Отмена невозможна.');
}
if (proposal.canceled) {
throw new Error('Предложение уже отменено. Повторная отмена невозможна.');
}
// Проверяем, что предложение активно (Pending)
if (proposal.state !== 0) {
const statusText = getProposalStatusText(proposal.state);
throw new Error(`Предложение не активно (статус: ${statusText}). Отмена возможна только для активных предложений.`);
}
// Проверяем, что пользователь является инициатором
if (proposal.initiator !== userAddress.value) {
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 минут до окончания голосования.');
}
}
// Отменяем предложение через готовую функцию из utils/dle-contract.js
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
console.log('✅ Предложение успешно отменено:', result.txHash);
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
// Принудительно обновляем состояние предложения в UI
updateProposalState(proposalId, {
canceled: true,
state: 2, // Отменено
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) => {
return proposal.state === 0; // Pending - только активные предложения
};
const canExecute = (proposal) => {
return proposal.state === 5; // ReadyForExecution - готово к выполнению
};
const canCancel = (proposal) => {
// Можно отменить только активные предложения (Pending)
return proposal.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();
}
};
return {
proposals,
filteredProposals,
isLoading,
isVoting,
isExecuting,
isCancelling,
statusFilter,
searchQuery,
loadProposals,
filterProposals,
voteOnProposal,
executeProposal,
cancelProposal,
getProposalStatusClass,
getProposalStatusText,
getQuorumPercentage,
getRequiredQuorumPercentage,
canVote,
canExecute,
canCancel,
updateProposalState,
// Валидация
validationStats,
isValidating
};
}