Files
DLE/frontend/src/composables/useProposalValidation.js
2026-03-01 22:03:48 +03:00

308 lines
12 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-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
};
}