308 lines
12 KiB
JavaScript
308 lines
12 KiB
JavaScript
/**
|
||
* 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
|
||
};
|
||
}
|