ваше сообщение коммита
This commit is contained in:
@@ -30,6 +30,12 @@ const userAccessLevel = ref({ level: 'user', tokenCount: 0, hasAccess: false });
|
||||
const updateIdentities = async () => {
|
||||
if (!isAuthenticated.value || !userId.value) return;
|
||||
|
||||
// Проверяем, что identities ref существует
|
||||
if (!identities || typeof identities.value === 'undefined') {
|
||||
console.warn('Identities ref is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/auth/identities');
|
||||
if (response.data.success) {
|
||||
@@ -46,14 +52,26 @@ const updateIdentities = async () => {
|
||||
}, []);
|
||||
|
||||
// Сравниваем новый отфильтрованный список с текущим значением
|
||||
const currentProviders = identities.value.map(id => id.provider).sort();
|
||||
const newProviders = filteredIdentities.map(id => id.provider).sort();
|
||||
const currentProviders = (identities.value || []).map(id => id?.provider || '').sort();
|
||||
const newProviders = (filteredIdentities || []).map(id => id?.provider || '').sort();
|
||||
|
||||
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
|
||||
|
||||
// Обновляем реактивное значение
|
||||
identities.value = filteredIdentities;
|
||||
console.log('User identities updated:', identities.value);
|
||||
// Обновляем реактивное значение с проверкой
|
||||
try {
|
||||
if (identities && identities.value !== undefined) {
|
||||
identities.value = filteredIdentities;
|
||||
console.log('User identities updated:', identities.value);
|
||||
} else {
|
||||
console.warn('Identities ref is not available or not initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating identities:', error);
|
||||
// Если произошла ошибка, пытаемся инициализировать identities
|
||||
if (identities && typeof identities.value === 'undefined') {
|
||||
identities.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
|
||||
// чтобы обновить authType и другие связанные данные (например, telegramId)
|
||||
@@ -163,11 +181,21 @@ const updateAuth = async ({
|
||||
|
||||
// Обновляем идентификаторы при любом изменении аутентификации
|
||||
if (authenticated) {
|
||||
await updateIdentities();
|
||||
startIdentitiesPolling();
|
||||
try {
|
||||
await updateIdentities();
|
||||
startIdentitiesPolling();
|
||||
} catch (error) {
|
||||
console.error('Error updating identities in updateAuth:', error);
|
||||
}
|
||||
} else {
|
||||
stopIdentitiesPolling();
|
||||
identities.value = [];
|
||||
try {
|
||||
if (identities && typeof identities.value !== 'undefined') {
|
||||
identities.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing identities:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Auth updated:', {
|
||||
@@ -306,7 +334,11 @@ const checkAuth = async () => {
|
||||
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
||||
if (response.data.authenticated) {
|
||||
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
|
||||
await updateIdentities();
|
||||
try {
|
||||
await updateIdentities();
|
||||
} catch (error) {
|
||||
console.error('Error updating identities in checkAuth:', error);
|
||||
}
|
||||
|
||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||
// связываем гостевые сообщения с его аккаунтом
|
||||
|
||||
349
frontend/src/composables/useDleContract.js
Normal file
349
frontend/src/composables/useDleContract.js
Normal file
@@ -0,0 +1,349 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { ethers } from 'ethers';
|
||||
import { DLE_ABI, TOKEN_ABI } from '@/utils/dle-abi';
|
||||
|
||||
/**
|
||||
* Композабл для работы с DLE смарт-контрактом
|
||||
* Содержит правильные ABI и функции для взаимодействия с контрактом
|
||||
*/
|
||||
export function useDleContract() {
|
||||
// Состояние
|
||||
const isConnected = ref(false);
|
||||
const provider = ref(null);
|
||||
const signer = ref(null);
|
||||
const contract = ref(null);
|
||||
const userAddress = ref(null);
|
||||
const chainId = ref(null);
|
||||
|
||||
// Используем общий ABI из utils/dle-abi.js
|
||||
|
||||
/**
|
||||
* Подключиться к кошельку
|
||||
*/
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||
}
|
||||
|
||||
// Запрашиваем подключение
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
|
||||
// Создаем провайдер и подписанта
|
||||
provider.value = new ethers.BrowserProvider(window.ethereum);
|
||||
signer.value = await provider.value.getSigner();
|
||||
userAddress.value = await signer.value.getAddress();
|
||||
|
||||
// Получаем информацию о сети
|
||||
const network = await provider.value.getNetwork();
|
||||
chainId.value = Number(network.chainId);
|
||||
|
||||
isConnected.value = true;
|
||||
|
||||
console.log('✅ Кошелек подключен:', {
|
||||
address: userAddress.value,
|
||||
chainId: chainId.value,
|
||||
network: network.name
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
address: userAddress.value,
|
||||
chainId: chainId.value
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка подключения к кошельку:', error);
|
||||
isConnected.value = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Инициализировать контракт
|
||||
*/
|
||||
const initContract = (contractAddress) => {
|
||||
if (!provider.value) {
|
||||
throw new Error('Провайдер не инициализирован. Сначала подключите кошелек.');
|
||||
}
|
||||
|
||||
contract.value = new ethers.Contract(contractAddress, DLE_ABI, signer.value);
|
||||
console.log('✅ DLE контракт инициализирован:', contractAddress);
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверить баланс токенов пользователя
|
||||
*/
|
||||
const checkTokenBalance = async (contractAddress) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const balance = await contract.value.balanceOf(userAddress.value);
|
||||
const balanceFormatted = ethers.formatEther(balance);
|
||||
|
||||
console.log(`💰 Баланс токенов: ${balanceFormatted}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
balance: balanceFormatted,
|
||||
hasTokens: balance > 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка проверки баланса:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
balance: '0',
|
||||
hasTokens: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Голосовать за предложение
|
||||
*/
|
||||
const voteOnProposal = async (contractAddress, proposalId, support) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
console.log('🗳️ Начинаем голосование:', { proposalId, support });
|
||||
|
||||
// Проверяем баланс токенов
|
||||
const balanceCheck = await checkTokenBalance(contractAddress);
|
||||
if (!balanceCheck.hasTokens) {
|
||||
throw new Error('У вас нет токенов для голосования');
|
||||
}
|
||||
|
||||
// Отправляем транзакцию голосования
|
||||
const tx = await contract.value.vote(proposalId, support);
|
||||
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||
|
||||
// Ждем подтверждения
|
||||
const receipt = await tx.wait();
|
||||
console.log('✅ Голосование успешно:', receipt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: tx.hash,
|
||||
receipt: receipt
|
||||
};
|
||||
} 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 = 'Недостаточно средств для оплаты газа';
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
originalError: error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Исполнить предложение
|
||||
*/
|
||||
const executeProposal = async (contractAddress, proposalId) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
console.log('⚡ Исполняем предложение:', proposalId);
|
||||
|
||||
const tx = await contract.value.executeProposal(proposalId);
|
||||
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
console.log('✅ Предложение исполнено:', receipt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: tx.hash,
|
||||
receipt: receipt
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка исполнения предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
originalError: error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отменить предложение
|
||||
*/
|
||||
const cancelProposal = async (contractAddress, proposalId, reason) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
console.log('❌ Отменяем предложение:', { proposalId, reason });
|
||||
|
||||
const tx = await contract.value.cancelProposal(proposalId, reason);
|
||||
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
console.log('✅ Предложение отменено:', receipt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: tx.hash,
|
||||
receipt: receipt
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка отмены предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
originalError: error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить состояние предложения
|
||||
*/
|
||||
const getProposalState = async (contractAddress, proposalId) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const state = await contract.value.getProposalState(proposalId);
|
||||
|
||||
// 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution
|
||||
const stateNames = {
|
||||
0: 'Pending',
|
||||
1: 'Succeeded',
|
||||
2: 'Defeated',
|
||||
3: 'Executed',
|
||||
4: 'Canceled',
|
||||
5: 'ReadyForExecution'
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: state,
|
||||
stateName: stateNames[state] || 'Unknown'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения состояния предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
state: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверить результат предложения
|
||||
*/
|
||||
const checkProposalResult = async (contractAddress, proposalId) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const result = await contract.value.checkProposalResult(proposalId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
passed: result.passed,
|
||||
quorumReached: result.quorumReached
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка проверки результата предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
passed: false,
|
||||
quorumReached: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить информацию о DLE
|
||||
*/
|
||||
const getDleInfo = async (contractAddress) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const info = await contract.value.getDLEInfo();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
name: info.name,
|
||||
symbol: info.symbol,
|
||||
location: info.location,
|
||||
coordinates: info.coordinates,
|
||||
jurisdiction: info.jurisdiction,
|
||||
okvedCodes: info.okvedCodes,
|
||||
kpp: info.kpp,
|
||||
creationTimestamp: info.creationTimestamp,
|
||||
isActive: info.isActive
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения информации о DLE:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Вычисляемые свойства
|
||||
const isWalletConnected = computed(() => isConnected.value);
|
||||
const currentUserAddress = computed(() => userAddress.value);
|
||||
const currentChainId = computed(() => chainId.value);
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
isConnected,
|
||||
provider,
|
||||
signer,
|
||||
contract,
|
||||
userAddress,
|
||||
chainId,
|
||||
|
||||
// Вычисляемые свойства
|
||||
isWalletConnected,
|
||||
currentUserAddress,
|
||||
currentChainId,
|
||||
|
||||
// Методы
|
||||
connectWallet,
|
||||
initContract,
|
||||
checkTokenBalance,
|
||||
voteOnProposal,
|
||||
executeProposal,
|
||||
cancelProposal,
|
||||
getProposalState,
|
||||
checkProposalResult,
|
||||
getDleInfo
|
||||
};
|
||||
}
|
||||
207
frontend/src/composables/useProposalValidation.js
Normal file
207
frontend/src/composables/useProposalValidation.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Composable для валидации предложений DLE
|
||||
* Проверяет реальность предложений по хешам транзакций
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export function useProposalValidation() {
|
||||
const validatedProposals = ref([]);
|
||||
const validationErrors = ref([]);
|
||||
const isValidating = ref(false);
|
||||
|
||||
// Проверка формата хеша транзакции
|
||||
const isValidTransactionHash = (hash) => {
|
||||
if (!hash) return false;
|
||||
return /^0x[a-fA-F0-9]{64}$/.test(hash);
|
||||
};
|
||||
|
||||
// Проверка формата адреса
|
||||
const isValidAddress = (address) => {
|
||||
if (!address) return false;
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
};
|
||||
|
||||
// Проверка chainId
|
||||
const isValidChainId = (chainId) => {
|
||||
const validChainIds = [1, 11155111, 17000, 421614, 84532, 8453]; // Mainnet, Sepolia, Holesky, Arbitrum Sepolia, Base Sepolia, Base
|
||||
return validChainIds.includes(Number(chainId));
|
||||
};
|
||||
|
||||
// Валидация предложения
|
||||
const validateProposal = (proposal) => {
|
||||
const errors = [];
|
||||
|
||||
// Проверка обязательных полей
|
||||
if (!proposal.id && proposal.id !== 0) {
|
||||
errors.push('Отсутствует ID предложения');
|
||||
}
|
||||
|
||||
if (!proposal.description || proposal.description.trim() === '') {
|
||||
errors.push('Отсутствует описание предложения');
|
||||
}
|
||||
|
||||
if (!proposal.transactionHash) {
|
||||
errors.push('Отсутствует хеш транзакции');
|
||||
} else if (!isValidTransactionHash(proposal.transactionHash)) {
|
||||
errors.push('Неверный формат хеша транзакции');
|
||||
}
|
||||
|
||||
if (!proposal.initiator) {
|
||||
errors.push('Отсутствует инициатор предложения');
|
||||
} else if (!isValidAddress(proposal.initiator)) {
|
||||
errors.push('Неверный формат адреса инициатора');
|
||||
}
|
||||
|
||||
if (!proposal.chainId) {
|
||||
errors.push('Отсутствует chainId');
|
||||
} else if (!isValidChainId(proposal.chainId)) {
|
||||
errors.push('Неподдерживаемый chainId');
|
||||
}
|
||||
|
||||
if (proposal.state === undefined || proposal.state === null) {
|
||||
errors.push('Отсутствует статус предложения');
|
||||
}
|
||||
|
||||
// Проверка числовых значений
|
||||
if (typeof proposal.forVotes !== 'number' || proposal.forVotes < 0) {
|
||||
errors.push('Неверное значение голосов "за"');
|
||||
}
|
||||
|
||||
if (typeof proposal.againstVotes !== 'number' || proposal.againstVotes < 0) {
|
||||
errors.push('Неверное значение голосов "против"');
|
||||
}
|
||||
|
||||
if (typeof proposal.quorumRequired !== 'number' || proposal.quorumRequired < 0) {
|
||||
errors.push('Неверное значение требуемого кворума');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
// Валидация массива предложений
|
||||
const validateProposals = (proposals) => {
|
||||
isValidating.value = true;
|
||||
validationErrors.value = [];
|
||||
validatedProposals.value = [];
|
||||
|
||||
const validProposals = [];
|
||||
const allErrors = [];
|
||||
|
||||
proposals.forEach((proposal, index) => {
|
||||
const validation = validateProposal(proposal);
|
||||
|
||||
if (validation.isValid) {
|
||||
validProposals.push(proposal);
|
||||
} else {
|
||||
allErrors.push({
|
||||
proposalIndex: index,
|
||||
proposalId: proposal.id,
|
||||
errors: validation.errors
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
validatedProposals.value = validProposals;
|
||||
validationErrors.value = allErrors;
|
||||
isValidating.value = false;
|
||||
|
||||
console.log(`[Proposal Validation] Проверено предложений: ${proposals.length}`);
|
||||
console.log(`[Proposal Validation] Валидных: ${validProposals.length}`);
|
||||
console.log(`[Proposal Validation] С ошибками: ${allErrors.length}`);
|
||||
|
||||
return {
|
||||
validProposals,
|
||||
errors: allErrors,
|
||||
totalCount: proposals.length,
|
||||
validCount: validProposals.length,
|
||||
errorCount: allErrors.length
|
||||
};
|
||||
};
|
||||
|
||||
// Получение статистики валидации
|
||||
const validationStats = computed(() => {
|
||||
const total = validatedProposals.value.length + validationErrors.value.length;
|
||||
const valid = validatedProposals.value.length;
|
||||
const invalid = validationErrors.value.length;
|
||||
|
||||
return {
|
||||
total,
|
||||
valid,
|
||||
invalid,
|
||||
validPercentage: total > 0 ? Math.round((valid / total) * 100) : 0,
|
||||
invalidPercentage: total > 0 ? Math.round((invalid / total) * 100) : 0
|
||||
};
|
||||
});
|
||||
|
||||
// Проверка, является ли предложение реальным (на основе хеша транзакции)
|
||||
const isRealProposal = (proposal) => {
|
||||
if (!proposal.transactionHash) return false;
|
||||
|
||||
// Проверяем, что хеш имеет правильный формат
|
||||
if (!isValidTransactionHash(proposal.transactionHash)) return false;
|
||||
|
||||
// Проверяем, что это не тестовые/фейковые хеши
|
||||
const fakeHashes = [
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||
];
|
||||
|
||||
if (fakeHashes.includes(proposal.transactionHash.toLowerCase())) return false;
|
||||
|
||||
// Проверяем, что хеш не начинается с нулей (подозрительно)
|
||||
if (proposal.transactionHash.startsWith('0x0000')) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Фильтрация только реальных предложений
|
||||
const filterRealProposals = (proposals) => {
|
||||
return proposals.filter(proposal => isRealProposal(proposal));
|
||||
};
|
||||
|
||||
// Фильтрация активных предложений (исключает выполненные и отмененные)
|
||||
const filterActiveProposals = (proposals) => {
|
||||
return proposals.filter(proposal => {
|
||||
// Исключаем выполненные и отмененные предложения
|
||||
if (proposal.executed || proposal.canceled) {
|
||||
console.log(`🚫 [FILTER] Исключаем предложение ${proposal.id}: executed=${proposal.executed}, canceled=${proposal.canceled}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Исключаем предложения с истекшим deadline
|
||||
if (proposal.deadline) {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (currentTime > proposal.deadline) {
|
||||
console.log(`⏰ [FILTER] Исключаем предложение ${proposal.id}: deadline истек`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
// Данные
|
||||
validatedProposals,
|
||||
validationErrors,
|
||||
isValidating,
|
||||
validationStats,
|
||||
|
||||
// Методы
|
||||
validateProposal,
|
||||
validateProposals,
|
||||
isRealProposal,
|
||||
filterRealProposals,
|
||||
filterActiveProposals,
|
||||
|
||||
// Вспомогательные функции
|
||||
isValidTransactionHash,
|
||||
isValidAddress,
|
||||
isValidChainId
|
||||
};
|
||||
}
|
||||
534
frontend/src/composables/useProposals.js
Normal file
534
frontend/src/composables/useProposals.js
Normal file
@@ -0,0 +1,534 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user