/** * 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 */ /** * 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('Отсутствует описание предложения'); } // Для мультичейн предложений проверяем chains // Если есть chains массив (даже с одним элементом), используем валидацию через chains const hasChains = proposal.chains && Array.isArray(proposal.chains) && proposal.chains.length > 0; if (hasChains) { // Для мультичейн предложений проверяем, что есть хотя бы одна валидная цепочка if (proposal.chains.length === 0) { errors.push('Мультичейн предложение не содержит цепочек'); } else { // Проверяем каждую цепочку let validChainsCount = 0; proposal.chains.forEach((chain, chainIndex) => { const chainErrors = []; if (!chain.id && chain.id !== 0) { chainErrors.push(`Цепочка ${chainIndex}: отсутствует ID`); } if (!chain.chainId) { chainErrors.push(`Цепочка ${chainIndex}: отсутствует chainId`); } else if (!isValidChainId(chain.chainId)) { chainErrors.push(`Цепочка ${chainIndex}: неподдерживаемый chainId ${chain.chainId}`); } if (!chain.transactionHash) { chainErrors.push(`Цепочка ${chainIndex}: отсутствует хеш транзакции`); } else if (!isValidTransactionHash(chain.transactionHash)) { chainErrors.push(`Цепочка ${chainIndex}: неверный формат хеша транзакции`); } if (chain.state === undefined || chain.state === null) { chainErrors.push(`Цепочка ${chainIndex}: отсутствует статус`); } if (typeof chain.forVotes !== 'number' || chain.forVotes < 0) { chainErrors.push(`Цепочка ${chainIndex}: неверное значение голосов "за"`); } if (typeof chain.againstVotes !== 'number' || chain.againstVotes < 0) { chainErrors.push(`Цепочка ${chainIndex}: неверное значение голосов "против"`); } if (typeof chain.quorumRequired !== 'number' || chain.quorumRequired < 0) { chainErrors.push(`Цепочка ${chainIndex}: неверное значение требуемого кворума`); } if (chainErrors.length === 0) { validChainsCount++; } else { errors.push(...chainErrors); } }); if (validChainsCount === 0) { errors.push('Мультичейн предложение не содержит валидных цепочек'); } } } else { // Для одиночных предложений проверяем стандартные поля if (!proposal.transactionHash) { errors.push('Отсутствует хеш транзакции'); } else if (!isValidTransactionHash(proposal.transactionHash)) { errors.push('Неверный формат хеша транзакции'); } if (!proposal.chainId) { errors.push('Отсутствует chainId'); } else if (!isValidChainId(proposal.chainId)) { errors.push('Неподдерживаемый chainId'); } // Проверка числовых значений для одиночных предложений 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('Неверное значение требуемого кворума'); } } if (!proposal.initiator) { errors.push('Отсутствует инициатор предложения'); } else if (!isValidAddress(proposal.initiator)) { errors.push('Неверный формат адреса инициатора'); } if (proposal.state === undefined || proposal.state === null) { 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, description: proposal.description, hasChains: !!(proposal.chains && Array.isArray(proposal.chains)), chainsCount: proposal.chains?.length || 0, 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}`); // Логируем ошибки для отладки if (allErrors.length > 0) { console.log(`[Proposal Validation] Детали ошибок:`, allErrors); allErrors.forEach((error, idx) => { console.log(`[Proposal Validation] Предложение ${idx + 1} (ID: ${error.proposalId}, описание: "${error.description || 'N/A'}"):`, { hasChains: error.hasChains, chainsCount: error.chainsCount, errors: error.errors }); }); } 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 }; }); // Проверка, является ли предложение реальным (на основе хеша транзакции) // Важно: после группировки мультичейн-предложений хеши транзакций могут жить только в proposal.chains[].transactionHash, // поэтому проверяем и верхний уровень, и цепочки. const isRealProposal = (proposal) => { const isRealTxHash = (txHash) => { if (!txHash || typeof txHash !== 'string') return false; // Проверяем, что хеш имеет правильный формат if (!isValidTransactionHash(txHash)) return false; const lower = txHash.toLowerCase(); // Проверяем, что это не тестовые/фейковые хеши const fakeHashes = [ '0x0000000000000000000000000000000000000000000000000000000000000000', '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' ]; if (fakeHashes.includes(lower)) return false; return true; }; // 1) Одиночные предложения (или если бэкенд положил хеш на верхний уровень) if (isRealTxHash(proposal?.transactionHash)) return true; // 2) Сгруппированные предложения: проверяем любую цепочку if (proposal?.chains && Array.isArray(proposal.chains) && proposal.chains.length > 0) { return proposal.chains.some(chain => isRealTxHash(chain?.transactionHash)); } return false; }; // Фильтрация только реальных предложений 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 }; }