ваше сообщение коммита

This commit is contained in:
2025-12-30 23:43:54 +03:00
parent 546e92ffb2
commit ea059565f9
15 changed files with 1975 additions and 302 deletions

View File

@@ -53,10 +53,89 @@ export function useProposalValidation() {
errors.push('Отсутствует описание предложения');
}
if (!proposal.transactionHash) {
errors.push('Отсутствует хеш транзакции');
} else if (!isValidTransactionHash(proposal.transactionHash)) {
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) {
@@ -65,29 +144,10 @@ export function useProposalValidation() {
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
@@ -112,6 +172,9 @@ export function useProposalValidation() {
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
});
}
@@ -124,6 +187,18 @@ export function useProposalValidation() {
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,
@@ -150,24 +225,37 @@ export function useProposalValidation() {
});
// Проверка, является ли предложение реальным (на основе хеша транзакции)
// Важно: после группировки мультичейн-предложений хеши транзакций могут жить только в proposal.chains[].transactionHash,
// поэтому проверяем и верхний уровень, и цепочки.
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 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;
};
// Фильтрация только реальных предложений

View File

@@ -15,7 +15,7 @@ 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';
import axios from 'axios';
import api from '@/api/axios';
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
// Функция checkTokenBalance перенесена в useDleContract.js
@@ -64,7 +64,7 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
// Получаем информацию о всех DLE в разных цепочках
console.log('[Proposals] Получаем информацию о всех DLE...');
const dleResponse = await axios.get('/api/dle-v2');
const dleResponse = await api.get('/dle-v2');
if (!dleResponse.data.success) {
console.error('Не удалось получить список DLE');
@@ -81,13 +81,31 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
for (const dle of allDles) {
if (!dle.networks || dle.networks.length === 0) continue;
// КРИТИЧНО: Пропускаем DLE, если ни один из его адресов не совпадает с запрошенным dleAddress
const hasMatchingAddress = dle.networks.some(network =>
network.address && network.address.toLowerCase() === (dleAddress.value || '').toLowerCase()
);
if (dleAddress.value && !hasMatchingAddress) {
console.log(`[Proposals] Пропускаем DLE ${dle.dleAddress || 'N/A'}: адрес ${dleAddress.value} не найден в networks`);
continue;
}
for (const network of dle.networks) {
try {
console.log(`[Proposals] Загружаем предложения из цепочки ${network.chainId}, адрес: ${network.address}`);
const response = await getProposals(network.address);
console.log(`[Proposals] Ответ для цепочки ${network.chainId}:`, {
success: response.success,
proposalsCount: response.data?.proposals?.length || 0,
hasError: !!response.error
});
if (response.success) {
const chainProposals = response.data.proposals || [];
// Бэкенд возвращает: { success: true, data: { proposals: [...], totalCount: ... } }
const chainProposals = (response.data?.data?.proposals || response.data?.proposals || []);
console.log(`[Proposals] Получено предложений для цепочки ${network.chainId}: ${chainProposals.length}`, chainProposals);
// Добавляем информацию о цепочке к каждому предложению
chainProposals.forEach(proposal => {
@@ -95,46 +113,135 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
proposal.contractAddress = network.address;
proposal.networkName = getChainName(network.chainId);
// Группируем предложения по описанию
// Группируем предложения по описанию и инициатору
const key = `${proposal.description}_${proposal.initiator}`;
// Преобразуем время создания в число для сравнения
// createdAt может быть ISO строкой или числом, timestamp - число в секундах
const getTimestamp = (proposal) => {
if (proposal.timestamp) return Number(proposal.timestamp); // timestamp в секундах
if (proposal.createdAt) {
if (typeof proposal.createdAt === 'string') {
return Math.floor(new Date(proposal.createdAt).getTime() / 1000); // ISO строка -> секунды
}
return Number(proposal.createdAt);
}
return Math.floor(Date.now() / 1000);
};
const proposalTimestamp = getTimestamp(proposal);
if (!proposalsByDescription.has(key)) {
proposalsByDescription.set(key, {
id: proposal.id,
id: proposal.id, // ID из первой найденной сети
description: proposal.description,
initiator: proposal.initiator,
deadline: proposal.deadline,
chains: new Map(),
createdAt: Math.min(...chainProposals.map(p => p.createdAt || Date.now())),
createdAt: proposalTimestamp, // Время создания в секундах
uniqueId: key
});
}
// Добавляем информацию о цепочке
proposalsByDescription.get(key).chains.set(network.chainId, {
...proposal,
chainId: network.chainId,
contractAddress: network.address,
networkName: getChainName(network.chainId)
});
const group = proposalsByDescription.get(key);
// Если в этой сети уже есть предложение с таким ключом, выбираем более позднее (актуальное)
const existingChainData = group.chains.get(network.chainId);
// Унифицируем state - всегда число
const normalizedState = typeof proposal.state === 'string'
? (proposal.state === 'active' ? 0 : NaN)
: Number(proposal.state);
// Убеждаемся, что id есть (fallback к proposalId из события, если id отсутствует)
const proposalId = proposal.id !== undefined && proposal.id !== null
? Number(proposal.id)
: (proposal.proposalId !== undefined ? Number(proposal.proposalId) : null);
if (existingChainData) {
// Если уже есть предложение в этой сети, сравниваем по времени создания
const existingTime = getTimestamp(existingChainData);
// Оставляем более позднее предложение (актуальное)
if (proposalTimestamp > existingTime) {
group.chains.set(network.chainId, {
...proposal,
id: proposalId !== null ? proposalId : existingChainData.id, // Используем id с fallback
chainId: network.chainId,
contractAddress: network.address,
networkName: getChainName(network.chainId),
state: normalizedState, // Унифицированный state (число)
timestamp: proposalTimestamp // Сохраняем числовой timestamp для удобства
});
}
// Иначе оставляем существующее
} else {
// Первое предложение в этой сети для данной группы
group.chains.set(network.chainId, {
...proposal,
id: proposalId !== null ? proposalId : 0, // Fallback к 0, если id отсутствует
chainId: network.chainId,
contractAddress: network.address,
networkName: getChainName(network.chainId),
state: normalizedState, // Унифицированный state (число)
timestamp: proposalTimestamp // Сохраняем числовой timestamp для удобства
});
}
// Обновляем createdAt группы - минимальное время из всех предложений
// После добавления нового предложения пересчитываем минимальное время
const allChainTimes = Array.from(group.chains.values())
.map(c => getTimestamp(c));
group.createdAt = Math.min(...allChainTimes, proposalTimestamp);
});
}
} catch (error) {
} catch (error) {
console.error(`Ошибка загрузки предложений из цепочки ${network.chainId}:`, error);
console.error(`Детали ошибки для цепочки ${network.chainId}:`, {
chainId: network.chainId,
address: network.address,
errorMessage: error.message,
errorStack: error.stack
});
}
}
}
// Преобразуем в массив для отображения
const rawProposals = Array.from(proposalsByDescription.values()).map(group => ({
...group,
chains: Array.from(group.chains.values()),
// Общий статус - активен если есть хотя бы одно активное предложение
state: group.chains.some(c => c.state === 'active') ? 'active' : 'inactive',
// Общий executed - выполнен если выполнен во всех цепочках
executed: group.chains.every(c => c.executed),
// Общий canceled - отменен если отменен в любой цепочке
canceled: group.chains.some(c => c.canceled)
}));
const rawProposals = Array.from(proposalsByDescription.values()).map(group => {
const chainsArray = Array.from(group.chains.values()).map(chain => {
// Унифицируем state для каждого chain - всегда число
const normalizedState = typeof chain.state === 'string'
? (chain.state === 'active' ? 0 : NaN)
: Number(chain.state);
// Убеждаемся, что id есть (fallback)
const chainId = chain.id !== undefined && chain.id !== null
? Number(chain.id)
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
return {
...chain,
id: chainId !== null ? chainId : 0, // Fallback к 0, если id отсутствует
state: isNaN(normalizedState) ? 0 : normalizedState // Всегда число, fallback к 0
};
});
// Определяем общий state группы (число) - минимальный state из всех chains
const groupState = chainsArray.length > 0
? Math.min(...chainsArray.map(c => Number(c.state || 0)))
: 0;
return {
...group,
chains: chainsArray,
// Общий статус - число (0 = Active, 3 = Executed, 4 = Canceled, 5 = ReadyForExecution)
state: groupState,
// Общий executed - выполнен если выполнен во всех цепочках
executed: chainsArray.length > 0 && chainsArray.every(c => c.executed),
// Общий canceled - отменен если отменен в любой цепочке
canceled: chainsArray.some(c => c.canceled)
};
});
console.log(`[Proposals] Сгруппировано предложений: ${rawProposals.length}`);
console.log(`[Proposals] Детали группировки:`, rawProposals);
@@ -145,18 +252,20 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
// Фильтруем только реальные предложения
const realProposals = filterRealProposals(validationResult.validProposals);
// Фильтруем только активные предложения (исключаем выполненные и отмененные)
const activeProposals = filterActiveProposals(realProposals);
console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`);
console.log(`[Proposals] Реальных предложений: ${realProposals.length}`);
// Считаем активные только для статистики/логов (не выкидываем остальные из списка,
// иначе фильтр "Все/Выполненные/Отмененные" в UI никогда не покажет эти статусы).
const activeProposals = filterActiveProposals(realProposals);
console.log(`[Proposals] Активных предложений: ${activeProposals.length}`);
if (validationResult.errorCount > 0) {
console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`);
}
proposals.value = activeProposals;
// В UI должны попадать ВСЕ реальные предложения; дальше их фильтрует statusFilter/searchQuery
proposals.value = realProposals;
filterProposals();
} catch (error) {
console.error('Ошибка загрузки предложений:', error);
@@ -217,6 +326,12 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
throw new Error('Предложение не найдено');
}
// КРИТИЧЕСКИ ВАЖНО: Если предложение мультичейн, используем voteOnMultichainProposal
if (proposal.chains && proposal.chains.length > 1) {
console.log('🌐 [VOTE] Обнаружено мультичейн предложение, используем voteOnMultichainProposal');
return await voteOnMultichainProposal(proposal, support);
}
console.log('📊 [DEBUG] Данные предложения:', {
id: proposal.id,
state: proposal.state,
@@ -339,6 +454,12 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
throw new Error('Предложение не найдено');
}
// КРИТИЧЕСКИ ВАЖНО: Если предложение мультичейн, используем executeMultichainProposal
if (proposal.chains && proposal.chains.length > 1) {
console.log('🌐 [EXECUTE] Обнаружено мультичейн предложение, используем executeMultichainProposal');
return await executeMultichainProposal(proposal);
}
console.log('📊 [DEBUG] Данные предложения для выполнения:', {
id: proposal.id,
state: proposal.state,
@@ -417,7 +538,8 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
state: proposal.state,
executed: proposal.executed,
canceled: proposal.canceled,
deadline: proposal.deadline
deadline: proposal.deadline,
chains: proposal.chains?.length || 0
});
// Проверяем, что предложение можно отменить
@@ -429,14 +551,8 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
throw new Error('Предложение уже отменено. Повторная отмена невозможна.');
}
// Проверяем, что предложение активно (Pending)
if (proposal.state !== 0) {
const statusText = getProposalStatusText(proposal.state);
throw new Error(`Предложение не активно (статус: ${statusText}). Отмена возможна только для активных предложений.`);
}
// Проверяем, что пользователь является инициатором
if (proposal.initiator !== userAddress.value) {
if (proposal.initiator?.toLowerCase() !== userAddress.value?.toLowerCase()) {
throw new Error('Только инициатор предложения может его отменить.');
}
@@ -449,16 +565,108 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
}
}
// Отменяем предложение через готовую функцию из utils/dle-contract.js
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
console.log('✅ Предложение успешно отменено:', result.txHash);
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
// КРИТИЧЕСКИ ВАЖНО: Мультичейн отмена - последовательно во всех активных сетях
if (proposal.chains && proposal.chains.length > 0) {
// Фильтруем только активные цепочки (можно отменить)
const activeChains = proposal.chains.filter(chain =>
canCancel(chain) && !chain.canceled && !chain.executed
);
if (activeChains.length === 0) {
throw new Error('Не найдено ни одной активной цепочки для отмены');
}
console.log(`🚀 [MULTI-CANCEL] Начинаем отмену в ${activeChains.length} цепочках последовательно...`);
const { switchToVotingNetwork } = await import('@/utils/dle-contract');
const results = [];
// КРИТИЧЕСКИ ВАЖНО: Отменяем ПОСЛЕДОВАТЕЛЬНО, а не параллельно!
// MetaMask может работать только с одной сетью одновременно
for (let index = 0; index < activeChains.length; index++) {
const chain = activeChains[index];
console.log(`📝 [${index + 1}/${activeChains.length}] Отмена в цепочке ${chain.networkName} (${chain.chainId})`);
try {
// Переключаемся на нужную сеть
console.log(`🔄 [${index + 1}/${activeChains.length}] Переключаемся на сеть ${chain.chainId}...`);
const switched = await switchToVotingNetwork(chain.chainId);
if (!switched) {
throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`);
}
// Задержка после переключения сети
await new Promise(resolve => setTimeout(resolve, 1000));
const contractAddress = chain.contractAddress || chain.address || dleAddress.value;
// Используем ID предложения из конкретной цепочки (с fallback)
let chainProposalId = chain.id !== undefined && chain.id !== null
? Number(chain.id)
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
// Fallback к proposalId, если chain.id отсутствует
if (chainProposalId === null || isNaN(chainProposalId)) {
chainProposalId = proposalId !== undefined && proposalId !== null ? Number(proposalId) : null;
}
if (chainProposalId === null || isNaN(chainProposalId)) {
throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposalId=${proposalId}`);
}
chainProposalId = Number(chainProposalId); // Убеждаемся, что это число
console.log(`🔍 [${index + 1}/${activeChains.length}] Используем ID предложения: ${chainProposalId} для отмены в цепочке ${chain.chainId}`);
// Отменяем предложение
console.log(`❌ [${index + 1}/${activeChains.length}] Отправляем отмену...`);
const result = await cancelProposalUtil(contractAddress, chainProposalId, reason);
console.log(`✅ [${index + 1}/${activeChains.length}] Предложение успешно отменено в ${chain.networkName}:`, result.txHash);
// Задержка после подтверждения транзакции (для Base Sepolia больше)
const delay = chain.chainId === 84532 ? 5000 : 3000;
await new Promise(resolve => setTimeout(resolve, delay));
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: true,
txHash: result.txHash
});
} catch (error) {
console.error(`❌ [${index + 1}/${activeChains.length}] Ошибка отмены в ${chain.networkName}:`, error);
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: false,
error: error.message
});
// Продолжаем отменять в других цепочках даже при ошибке
}
}
// Подводим итоги
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`📊 [MULTI-CANCEL] Отмена завершена: успешно в ${successful.length} из ${activeChains.length} цепочек`);
if (successful.length > 0) {
alert(`Предложение отменено в ${successful.length} из ${activeChains.length} цепочек!\n${failed.length > 0 ? `Ошибки в ${failed.length} цепочках.` : ''}`);
} else {
throw new Error('Не удалось отменить предложение ни в одной цепочке');
}
} else {
// Одиночное предложение (без мультичейн)
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
console.log('✅ Предложение успешно отменено:', result.txHash);
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
}
// Принудительно обновляем состояние предложения в UI
updateProposalState(proposalId, {
canceled: true,
state: 2, // Отменено
state: 4, // Canceled
executed: false
});
@@ -554,16 +762,32 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
};
const canVote = (proposal) => {
return proposal.state === 0; // Pending - только активные предложения
// Для мультичейн предложений используем canVoteMultichain
if (proposal.chains && proposal.chains.length > 1) {
return canVoteMultichain(proposal);
}
// Унифицируем state - всегда число
const state = typeof proposal.state === 'string'
? (proposal.state === 'active' ? 0 : NaN)
: Number(proposal.state);
return state === 0; // Pending - только активные предложения
};
const canExecute = (proposal) => {
return proposal.state === 5; // ReadyForExecution - готово к выполнению
// Унифицируем state - всегда число
const state = typeof proposal.state === 'string'
? (proposal.state === 'active' ? 0 : NaN)
: Number(proposal.state);
return state === 5; // ReadyForExecution - готово к выполнению
};
const canCancel = (proposal) => {
// Унифицируем state - всегда число
const state = typeof proposal.state === 'string'
? (proposal.state === 'active' ? 0 : NaN)
: Number(proposal.state);
// Можно отменить только активные предложения (Pending)
return proposal.state === 0 &&
return state === 0 &&
!proposal.executed &&
!proposal.canceled;
};
@@ -585,27 +809,110 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
try {
isVoting.value = true;
console.log(`🌐 [MULTI-VOTE] Начинаем голосование в ${proposal.chains.length} цепочках:`, proposal.chains.map(c => c.networkName));
// Фильтруем только активные цепочки (state === 0 или 'active', не выполнены, не отменены)
const activeChains = proposal.chains.filter(chain => canVote(chain));
if (activeChains.length === 0) {
throw new Error('Не найдено ни одной активной цепочки для голосования');
}
// Голосуем последовательно в каждой цепочке
for (const chain of proposal.chains) {
console.log(`🌐 [MULTI-VOTE] Начинаем голосование в ${activeChains.length} цепочках последовательно...`);
const { switchToVotingNetwork } = await import('@/utils/dle-contract');
const results = [];
// КРИТИЧЕСКИ ВАЖНО: Голосуем ПОСЛЕДОВАТЕЛЬНО, а не параллельно!
// MetaMask может работать только с одной сетью одновременно
for (let index = 0; index < activeChains.length; index++) {
const chain = activeChains[index];
console.log(`📝 [${index + 1}/${activeChains.length}] Голосование в цепочке ${chain.networkName} (${chain.chainId})`);
try {
console.log(`🎯 [MULTI-VOTE] Голосуем в ${chain.networkName} (${chain.contractAddress})`);
await voteForProposal(chain.contractAddress, chain.id, support);
console.log(`✅ [MULTI-VOTE] Голос отдан в ${chain.networkName}`);
// Небольшая задержка между голосованиями
// Переключаемся на нужную сеть
console.log(`🔄 [${index + 1}/${activeChains.length}] Переключаемся на сеть ${chain.chainId}...`);
const switched = await switchToVotingNetwork(chain.chainId);
if (!switched) {
throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`);
}
// Задержка после переключения сети
await new Promise(resolve => setTimeout(resolve, 1000));
const contractAddress = chain.contractAddress || chain.address || dleAddress.value;
// Используем ID предложения из конкретной цепочки (с fallback)
let chainProposalId = chain.id !== undefined && chain.id !== null
? Number(chain.id)
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
// Fallback к proposal.id, если chain.id отсутствует
if (chainProposalId === null || isNaN(chainProposalId)) {
chainProposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : null;
}
if (chainProposalId === null || isNaN(chainProposalId)) {
throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposal.id=${proposal.id}`);
}
chainProposalId = Number(chainProposalId); // Убеждаемся, что это число
console.log(`🔍 [${index + 1}/${activeChains.length}] Используем ID предложения: ${chainProposalId} для голосования в цепочке ${chain.chainId}`);
// Проверяем баланс токенов в каждой сети (балансы могут отличаться в разных сетях)
console.log(`💰 [${index + 1}/${activeChains.length}] Проверяем баланс токенов в ${chain.networkName}...`);
try {
const balanceCheck = await checkTokenBalance(contractAddress, userAddress.value);
console.log(`💰 [${index + 1}/${activeChains.length}] Баланс токенов в ${chain.networkName}:`, balanceCheck);
if (!balanceCheck.hasTokens) {
console.warn(`⚠️ [${index + 1}/${activeChains.length}] Нет токенов в ${chain.networkName}, пропускаем голосование в этой сети`);
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: false,
error: `Нет токенов DLE в сети ${chain.networkName} для голосования`
});
// Продолжаем с следующей сетью
continue;
}
} catch (balanceError) {
console.warn(`⚠️ [${index + 1}/${activeChains.length}] Ошибка проверки баланса в ${chain.networkName} (продолжаем):`, balanceError.message);
// При ошибке проверки баланса продолжаем попытку голосования
// Контракт сам проверит баланс и вернет ошибку, если токенов нет
}
// Голосуем
console.log(`🗳️ [${index + 1}/${activeChains.length}] Отправляем голосование для proposalId=${chainProposalId} в ${chain.networkName}...`);
const result = await voteForProposal(contractAddress, chainProposalId, support);
console.log(`✅ [${index + 1}/${activeChains.length}] Голосование успешно в ${chain.networkName}:`, result.txHash);
// Задержка после подтверждения транзакции (для Base Sepolia больше)
const delay = chain.chainId === 84532 ? 5000 : 3000;
await new Promise(resolve => setTimeout(resolve, delay));
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: true,
txHash: result.txHash
});
} catch (error) {
console.error(`❌ [MULTI-VOTE] Ошибка голосования в ${chain.networkName}:`, error);
// Продолжаем голосовать в других цепочках даже при ошибке в одной
console.error(`❌ [${index + 1}/${activeChains.length}] Ошибка голосования в ${chain.networkName}:`, error);
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: false,
error: error.message
});
// Продолжаем голосовать в других цепочках даже при ошибке
}
}
console.log('🎉 [MULTI-VOTE] Голосование завершено во всех цепочках');
// Подводим итоги
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`📊 [MULTI-VOTE] Голосование завершено: успешно в ${successful.length} из ${activeChains.length} цепочек`);
// Перезагружаем предложения
await loadProposals();
@@ -622,26 +929,87 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
try {
isExecuting.value = true;
console.log(`🚀 [MULTI-EXECUTE] Начинаем исполнение в ${proposal.chains.length} цепочках`);
// Фильтруем только готовые к выполнению цепочки
const readyChains = proposal.chains.filter(chain => canExecute(chain));
if (readyChains.length === 0) {
throw new Error('Нет цепочек, готовых к выполнению');
}
// Исполняем параллельно во всех цепочках
const executePromises = proposal.chains.map(async (chain) => {
console.log(`🚀 [MULTI-EXECUTE] Начинаем исполнение в ${readyChains.length} цепочках последовательно...`);
const { switchToVotingNetwork } = await import('@/utils/dle-contract');
const results = [];
// КРИТИЧЕСКИ ВАЖНО: Исполняем ПОСЛЕДОВАТЕЛЬНО, а не параллельно!
// MetaMask может работать только с одной сетью одновременно
for (let index = 0; index < readyChains.length; index++) {
const chain = readyChains[index];
console.log(`📝 [${index + 1}/${readyChains.length}] Выполнение в цепочке ${chain.networkName} (${chain.chainId})`);
try {
console.log(`🎯 [MULTI-EXECUTE] Исполняем в ${chain.networkName} (${chain.contractAddress})`);
await executeProposalUtil(chain.contractAddress, chain.id);
console.log(`✅ [MULTI-EXECUTE] Исполнено в ${chain.networkName}`);
// Переключаемся на нужную сеть
console.log(`🔄 [${index + 1}/${readyChains.length}] Переключаемся на сеть ${chain.chainId}...`);
const switched = await switchToVotingNetwork(chain.chainId);
if (!switched) {
throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`);
}
// Задержка после переключения сети
await new Promise(resolve => setTimeout(resolve, 1000));
const contractAddress = chain.contractAddress || chain.address || dleAddress.value;
// Используем ID предложения из конкретной цепочки (с fallback)
let chainProposalId = chain.id !== undefined && chain.id !== null
? Number(chain.id)
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
// Fallback к proposal.id, если chain.id отсутствует
if (chainProposalId === null || isNaN(chainProposalId)) {
chainProposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : null;
}
if (chainProposalId === null || isNaN(chainProposalId)) {
throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposal.id=${proposal.id}`);
}
chainProposalId = Number(chainProposalId); // Убеждаемся, что это число
console.log(`🔍 [${index + 1}/${readyChains.length}] Используем ID предложения: ${chainProposalId} для выполнения в цепочке ${chain.chainId}`);
// Выполняем предложение
console.log(`⚡ [${index + 1}/${readyChains.length}] Отправляем выполнение...`);
const result = await executeProposalUtil(contractAddress, chainProposalId);
console.log(`✅ [${index + 1}/${readyChains.length}] Предложение успешно выполнено в ${chain.networkName}:`, result.txHash);
// Задержка после подтверждения транзакции (для Base Sepolia больше)
const delay = chain.chainId === 84532 ? 5000 : 3000;
await new Promise(resolve => setTimeout(resolve, delay));
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: true,
txHash: result.txHash
});
} catch (error) {
console.error(`❌ [MULTI-EXECUTE] Ошибка исполнения в ${chain.networkName}:`, error);
// Продолжаем исполнение в других цепочках
console.error(`❌ [${index + 1}/${readyChains.length}] Ошибка выполнения в ${chain.networkName}:`, error);
results.push({
chainId: chain.chainId,
networkName: chain.networkName,
success: false,
error: error.message
});
// Продолжаем выполнять в других цепочках даже при ошибке
}
});
}
await Promise.all(executePromises);
console.log('🎉 [MULTI-EXECUTE] Исполнение завершено во всех цепочках');
// Подводим итоги
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`📊 [MULTI-EXECUTE] Выполнение завершено: успешно в ${successful.length} из ${readyChains.length} цепочек`);
// Перезагружаем предложения
await loadProposals();

View File

@@ -11,7 +11,7 @@
*/
// Сервис для работы с DLE v2 - основные функции
import axios from 'axios';
import api from '@/api/axios';
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
@@ -22,7 +22,7 @@ import axios from 'axios';
*/
export const getAllDLEs = async () => {
try {
const response = await axios.get('/dle-v2');
const response = await api.get('/dle-v2');
return response.data;
} catch (error) {
console.error('Ошибка при получении списка DLE:', error);
@@ -37,7 +37,7 @@ export const getAllDLEs = async () => {
*/
export const getDLEInfo = async (dleAddress) => {
try {
const response = await axios.get(`/dle-v2/${dleAddress}`);
const response = await api.get(`/dle-v2/${dleAddress}`);
return response.data;
} catch (error) {
console.error('Ошибка при получении информации о DLE:', error);
@@ -54,7 +54,7 @@ export const getDLEInfo = async (dleAddress) => {
*/
export const getGovernanceParams = async (dleAddress) => {
try {
const response = await axios.post('/dle-core/get-governance-params', { dleAddress });
const response = await api.post('/dle-core/get-governance-params', { dleAddress });
return response.data;
} catch (error) {
console.error('Ошибка при получении параметров управления:', error);
@@ -71,7 +71,7 @@ export const getGovernanceParams = async (dleAddress) => {
*/
export const getSupportedChains = async (dleAddress) => {
try {
const response = await axios.post('/dle-multichain/get-supported-chains', {
const response = await api.post('/dle-multichain/get-supported-chains', {
dleAddress
});
return response.data;

View File

@@ -11,7 +11,7 @@
*/
// Сервис для работы с предложениями DLE
import axios from 'axios';
import api from '@/api/axios';
/**
* Получает список всех предложений
@@ -21,7 +21,7 @@ import axios from 'axios';
export const getProposals = async (dleAddress) => {
try {
console.log(`🌐 [API] Запрашиваем предложения для DLE: ${dleAddress}`);
const response = await axios.post('/dle-proposals/get-proposals', { dleAddress });
const response = await api.post('/dle-proposals/get-proposals', { dleAddress });
console.log(`🌐 [API] Ответ от backend:`, {
success: response.data.success,
@@ -44,7 +44,7 @@ export const getProposals = async (dleAddress) => {
*/
export const getProposalInfo = async (dleAddress, proposalId) => {
try {
const response = await axios.post('/dle-proposals/get-proposal-info', {
const response = await api.post('/dle-proposals/get-proposal-info', {
dleAddress,
proposalId
});
@@ -63,7 +63,7 @@ export const getProposalInfo = async (dleAddress, proposalId) => {
*/
export const createProposal = async (dleAddress, proposalData) => {
try {
const response = await axios.post('/dle-proposals/create-proposal', {
const response = await api.post('/dle-proposals/create-proposal', {
dleAddress,
...proposalData
});
@@ -92,7 +92,7 @@ export const voteOnProposal = async (dleAddress, proposalId, support, userAddres
console.log('📤 [SERVICE] Отправляем запрос на голосование:', requestData);
const response = await axios.post('/dle-proposals/vote-proposal', requestData);
const response = await api.post('/dle-proposals/vote-proposal', requestData);
console.log('📥 [SERVICE] Ответ от бэкенда:', response.data);
@@ -111,7 +111,7 @@ export const voteOnProposal = async (dleAddress, proposalId, support, userAddres
*/
export const executeProposal = async (dleAddress, proposalId) => {
try {
const response = await axios.post('/dle-proposals/execute-proposal', {
const response = await api.post('/dle-proposals/execute-proposal', {
dleAddress,
proposalId
});
@@ -131,7 +131,7 @@ export const executeProposal = async (dleAddress, proposalId) => {
*/
export const cancelProposal = async (dleAddress, proposalId, reason) => {
try {
const response = await axios.post('/dle-proposals/cancel-proposal', {
const response = await api.post('/dle-proposals/cancel-proposal', {
dleAddress,
proposalId,
reason
@@ -151,7 +151,7 @@ export const cancelProposal = async (dleAddress, proposalId, reason) => {
*/
export const getProposalState = async (dleAddress, proposalId) => {
try {
const response = await axios.post('/dle-proposals/get-proposal-state', {
const response = await api.post('/dle-proposals/get-proposal-state', {
dleAddress,
proposalId
});
@@ -170,7 +170,7 @@ export const getProposalState = async (dleAddress, proposalId) => {
*/
export const getProposalVotes = async (dleAddress, proposalId) => {
try {
const response = await axios.post('/dle-proposals/get-proposal-votes', {
const response = await api.post('/dle-proposals/get-proposal-votes', {
dleAddress,
proposalId
});
@@ -189,7 +189,7 @@ export const getProposalVotes = async (dleAddress, proposalId) => {
*/
export const checkProposalResult = async (dleAddress, proposalId) => {
try {
const response = await axios.post('/dle-proposals/check-proposal-result', {
const response = await api.post('/dle-proposals/check-proposal-result', {
dleAddress,
proposalId
});
@@ -207,7 +207,7 @@ export const checkProposalResult = async (dleAddress, proposalId) => {
*/
export const getProposalsCount = async (dleAddress) => {
try {
const response = await axios.post('/dle-proposals/get-proposals-count', {
const response = await api.post('/dle-proposals/get-proposals-count', {
dleAddress
});
return response.data;
@@ -226,7 +226,7 @@ export const getProposalsCount = async (dleAddress) => {
*/
export const listProposals = async (dleAddress, offset = 0, limit = 10) => {
try {
const response = await axios.post('/dle-proposals/list-proposals', {
const response = await api.post('/dle-proposals/list-proposals', {
dleAddress,
offset,
limit
@@ -247,7 +247,7 @@ export const listProposals = async (dleAddress, offset = 0, limit = 10) => {
*/
export const getVotingPowerAt = async (dleAddress, voter, timepoint) => {
try {
const response = await axios.post('/dle-proposals/get-voting-power-at', {
const response = await api.post('/dle-proposals/get-voting-power-at', {
dleAddress,
voter,
timepoint
@@ -267,7 +267,7 @@ export const getVotingPowerAt = async (dleAddress, voter, timepoint) => {
*/
export const getQuorumAt = async (dleAddress, timepoint) => {
try {
const response = await axios.post('/dle-proposals/get-quorum-at', {
const response = await api.post('/dle-proposals/get-quorum-at', {
dleAddress,
timepoint
});
@@ -285,7 +285,7 @@ export const getQuorumAt = async (dleAddress, timepoint) => {
*/
export const decodeProposalData = async (transactionHash) => {
try {
const response = await axios.post('/dle-proposals/decode-proposal-data', {
const response = await api.post('/dle-proposals/decode-proposal-data', {
transactionHash
});
return response.data;

View File

@@ -17,7 +17,9 @@ import { DLE_ABI, DLE_DEACTIVATION_ABI, TOKEN_ABI } from './dle-abi';
// Функция для переключения сети кошелька
export async function switchToVotingNetwork(chainId) {
try {
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId}...`);
// Преобразуем chainId в строку для поиска в объекте networks
const chainIdStr = String(chainId);
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId} (строка: ${chainIdStr})...`);
// Конфигурации сетей
const networks = {
@@ -51,49 +53,53 @@ export async function switchToVotingNetwork(chainId) {
}
};
const networkConfig = networks[chainId];
const networkConfig = networks[chainIdStr];
if (!networkConfig) {
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId}`);
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId} (строка: ${chainIdStr})`);
console.error(`❌ [NETWORK] Доступные сети:`, Object.keys(networks));
return false;
}
// Проверяем, подключена ли уже нужная сеть
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
console.log(`🔍 [NETWORK] Текущая сеть: ${currentChainId}, нужная: ${networkConfig.chainId}`);
if (currentChainId === networkConfig.chainId) {
console.log(`✅ [NETWORK] Сеть ${chainId} уже подключена`);
console.log(`✅ [NETWORK] Сеть ${chainIdStr} уже подключена`);
return true;
}
// Пытаемся переключиться на нужную сеть
try {
console.log(`🔄 [NETWORK] Запрашиваем переключение на сеть ${chainIdStr}...`);
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: networkConfig.chainId }]
});
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainId}`);
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainIdStr}`);
return true;
} catch (switchError) {
console.error(`⚠️ [NETWORK] Ошибка переключения:`, switchError);
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log(` [NETWORK] Добавляем сеть ${chainId}...`);
console.log(` [NETWORK] Добавляем сеть ${chainIdStr}...`);
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [networkConfig]
});
console.log(`✅ [NETWORK] Сеть ${chainId} добавлена и подключена`);
console.log(`✅ [NETWORK] Сеть ${chainIdStr} добавлена и подключена`);
return true;
} catch (addError) {
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainId}:`, addError);
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainIdStr}:`, addError);
return false;
}
} else {
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainId}:`, switchError);
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainIdStr}:`, switchError);
return false;
}
}
} catch (error) {
console.error(`❌ [NETWORK] Общая ошибка переключения сети:`, error);
console.error(`❌ [NETWORK] Общая ошибка переключения сети ${chainIdStr}:`, error);
return false;
}
}
@@ -197,11 +203,11 @@ export async function createProposal(dleAddress, proposalData) {
const signer = await provider.getSigner();
// Используем общий ABI
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
// Создаем предложение
const tx = await dle.createProposal(
// Правильный порядок параметров: description, duration, operation, targetChains, timelockDelay
const tx = await dle.createProposal(
proposalData.description,
proposalData.duration,
proposalData.operation,
@@ -262,85 +268,17 @@ export async function voteForProposal(dleAddress, proposalId, support) {
console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования');
// Проверяем сеть голосования
// Проверяем право голоса (если доступно)
try {
const proposal = await dle.proposals(proposalId);
const currentChainId = await dle.getCurrentChainId();
const governanceChainId = proposal.governanceChainId;
console.log('🔍 [VOTE DEBUG] Текущая сеть контракта:', currentChainId.toString());
console.log('🔍 [VOTE DEBUG] Сеть голосования предложения:', governanceChainId.toString());
if (currentChainId.toString() !== governanceChainId.toString()) {
console.log('🔄 [VOTE DEBUG] Неправильная сеть! Пытаемся переключиться...');
// Пытаемся переключить сеть
const switched = await switchToVotingNetwork(governanceChainId.toString());
if (switched) {
console.log('✅ [VOTE DEBUG] Сеть успешно переключена, переподключаемся к контракту...');
// Определяем правильный адрес контракта для сети голосования
let correctContractAddress = dleAddress;
// Если контракт развернут в другой сети, нужно найти контракт в нужной сети
if (currentChainId.toString() !== governanceChainId.toString()) {
console.log('🔍 [VOTE DEBUG] Ищем контракт в сети голосования...');
try {
// Получаем информацию о мультичейн развертывании из БД
const response = await fetch('/api/dle-core/get-multichain-contracts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
originalContract: dleAddress,
targetChainId: governanceChainId.toString()
})
});
if (response.ok) {
const data = await response.json();
if (data.success && data.contractAddress) {
correctContractAddress = data.contractAddress;
console.log('🔍 [VOTE DEBUG] Найден контракт в сети голосования:', correctContractAddress);
} else {
console.warn('⚠️ [VOTE DEBUG] Контракт в сети голосования не найден, используем исходный');
}
} else {
console.warn('⚠️ [VOTE DEBUG] Ошибка получения контракта из БД, используем исходный');
}
} catch (error) {
console.warn('⚠️ [VOTE DEBUG] Ошибка поиска контракта, используем исходный:', error.message);
}
}
// Переподключаемся к контракту в новой сети
const newProvider = new ethers.BrowserProvider(window.ethereum);
const newSigner = await newProvider.getSigner();
dle = new ethers.Contract(correctContractAddress, DLE_ABI, newSigner);
// Проверяем, что теперь все корректно
const newCurrentChainId = await dle.getCurrentChainId();
console.log('🔍 [VOTE DEBUG] Новая текущая сеть контракта:', newCurrentChainId.toString());
if (newCurrentChainId.toString() === governanceChainId.toString()) {
console.log('✅ [VOTE DEBUG] Сеть для голосования теперь корректна');
} else {
throw new Error(`Не удалось переключиться на правильную сеть. Текущая: ${newCurrentChainId}, требуется: ${governanceChainId}`);
}
} else {
throw new Error(`Неправильная сеть! Контракт в сети ${currentChainId}, а голосование должно быть в сети ${governanceChainId}. Переключите кошелек вручную.`);
if (proposal.snapshotTimepoint) {
const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint);
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
if (votingPower === 0n) {
throw new Error('У пользователя нет права голоса (votingPower = 0)');
}
} else {
console.log('🔍 [VOTE DEBUG] Сеть для голосования корректна');
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
}
// Проверяем право голоса
const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint);
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
if (votingPower === 0n) {
throw new Error('У пользователя нет права голоса (votingPower = 0)');
}
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
} catch (votingPowerError) {
console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message);
}
@@ -1089,7 +1027,6 @@ export async function loadDeactivationProposals(dleAddress) {
* @param {number} transferData.amount - Количество токенов
* @param {string} transferData.description - Описание предложения
* @param {number} transferData.duration - Длительность голосования в секундах
* @param {number} transferData.governanceChainId - ID сети для голосования
* @param {Array<number>} transferData.targetChains - Целевые сети для исполнения
* @returns {Promise<Object>} - Результат создания предложения
*/
@@ -1109,15 +1046,19 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
// Получаем адрес отправителя (инициатора предложения)
const senderAddress = await signer.getAddress();
// Кодируем операцию перевода токенов
const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)");
const transferDataEncoded = ethers.AbiCoder.defaultAbiCoder().encode(
["address", "uint256"],
[transferData.recipient, ethers.parseUnits(transferData.amount.toString(), 18)]
);
// Объединяем селектор и данные
const operation = ethers.concat([transferFunctionSelector, transferDataEncoded]);
// Правильная сигнатура: _transferTokens(address,address,uint256)
// Параметры: sender (инициатор), recipient (получатель), amount (в wei)
const functionSignature = '_transferTokens(address,address,uint256)';
const iface = new ethers.Interface([`function ${functionSignature}`]);
const operation = iface.encodeFunctionData('_transferTokens', [
senderAddress, // адрес инициатора
transferData.recipient, // адрес получателя
ethers.parseUnits(transferData.amount.toString(), 18) // количество в wei
]);
console.log('Создание предложения о переводе токенов:', {
recipient: transferData.recipient,
@@ -1127,11 +1068,11 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
});
// Создаем предложение
// Правильный порядок параметров: description, duration, operation, targetChains, timelockDelay
const tx = await dle.createProposal(
transferData.description,
transferData.duration,
operation,
transferData.governanceChainId,
transferData.targetChains || [],
0 // timelockDelay
);

View File

@@ -48,7 +48,6 @@
<div class="default-logo" v-else>DLE</div>
<div class="dle-title">
<h3>{{ dle.name }} ({{ dle.symbol }})</h3>
<span class="dle-version">{{ dle.version || 'v2' }}</span>
</div>
</div>
</div>
@@ -64,12 +63,12 @@
<li v-for="net in dle.networks" :key="net.chainId" class="network-item">
<span class="chain-name">{{ getChainName(net.chainId) }}:</span>
<a
:href="getExplorerUrl(net.chainId, net.dleAddress)"
:href="getExplorerUrl(net.chainId, net.address)"
target="_blank"
class="address-link"
@click.stop
>
{{ shortenAddress(net.dleAddress) }}
{{ shortenAddress(net.address) }}
<i class="fas fa-external-link-alt"></i>
</a>
</li>

View File

@@ -574,7 +574,12 @@ const togglePreview = () => {
const closeSuccessModal = () => {
showSuccessModal.value = false;
goBackToProposals();
// Переход на страницу предложений после закрытия модалки
if (dleAddress.value) {
router.push(`/management/proposals?address=${dleAddress.value}`);
} else {
router.push('/management/proposals');
}
};
const openProposals = () => {

View File

@@ -44,7 +44,6 @@
<div class="operations-grid">
<!-- Основные операции DLE -->
<div class="operation-category">
<h5>Основные операции DLE</h5>
<div class="operation-blocks">
<div class="operation-block">
<h6>Передача токенов</h6>
@@ -197,7 +196,7 @@ onMounted(() => {
window.addEventListener('refresh-application-data', () => {
console.log('[CreateProposalView] Refreshing DLE proposal data');
loadDLEInfo(); // Обновляем данные при входе в систему
loadDleData(); // Обновляем данные при входе в систему
});
});

View File

@@ -106,13 +106,123 @@
<span>🔗</span>
<span>ID: {{ proposal.uniqueId }}</span>
</div>
<div class="meta-item">
<!-- Мульти-чейн информация -->
<div v-if="proposal.chains && proposal.chains.length > 1" class="meta-item multichain-info">
<span>🌐</span>
<span>Цепочки ({{ proposal.chains.length }}): {{ proposal.chains.map(c => c.networkName || `Chain ${c.chainId}`).join(', ') }}</span>
</div>
<div v-else class="meta-item">
<span></span>
<span>Chain: {{ proposal.chainId }}</span>
<span>Chain: {{ proposal.chainId ? (proposal.chains?.[0]?.networkName || `Chain ${proposal.chainId}`) : 'N/A' }}</span>
</div>
<div class="meta-item">
<span>📄</span>
<span>Hash: {{ (proposal.transactionHash || '').substring(0, 10) }}...</span>
<span>Hash: {{ ((proposal.transactionHash || proposal.chains?.[0]?.transactionHash || '')).substring(0, 10) }}...</span>
</div>
</div>
<!-- Детали по цепочкам для мульти-чейн предложений -->
<div v-if="proposal.chains && proposal.chains.length > 1" class="chains-details">
<div class="chains-header">
<strong>Статус по цепочкам:</strong>
</div>
<div class="chains-list">
<div
v-for="chain in proposal.chains"
:key="chain.chainId"
class="chain-item"
:class="{
'chain-active': Number(chain.state) === 0,
'chain-executed': chain.executed,
'chain-canceled': chain.canceled
}"
>
<div class="chain-main-info">
<span class="chain-name">{{ chain.networkName || `Chain ${chain.chainId}` }}</span>
<span class="chain-status">
<span v-if="chain.executed"> Выполнено</span>
<span v-else-if="chain.canceled"> Отменено</span>
<span v-else-if="chain.deadline && chain.deadline < Date.now() / 1000"> Истекло</span>
<span v-else-if="Number(chain.state) === 5">🟡 Готово к выполнению</span>
<span v-else-if="Number(chain.state) === 0">🟢 Активно</span>
<span v-else> {{ chain.state }}</span>
</span>
</div>
<div class="chain-details-info">
<div class="chain-detail-item">
<span class="detail-label">ID предложения:</span>
<span class="detail-value">#{{ chain.id !== undefined && chain.id !== null ? chain.id : 'N/A' }}</span>
</div>
<div class="chain-detail-item">
<span class="detail-label">Голоса:</span>
<span class="detail-value">
👍 {{ chain.forVotes ? (Number(chain.forVotes) / 1e18).toFixed(2) : '0.00' }} DLE |
👎 {{ chain.againstVotes ? (Number(chain.againstVotes) / 1e18).toFixed(2) : '0.00' }} DLE
</span>
</div>
<div class="chain-detail-item">
<span class="detail-label">Кворум:</span>
<span class="detail-value" :class="{ 'quorum-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) >= Number(chain.quorumRequired), 'quorum-not-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) < Number(chain.quorumRequired) }">
{{ chain.forVotes && chain.quorumRequired ?
(Number(chain.forVotes) >= Number(chain.quorumRequired) ? '✅ Достигнут' : '❌ Не достигнут') :
'N/A' }}
({{ chain.quorumRequired ? (Number(chain.quorumRequired) / 1e18).toFixed(2) : '0.00' }} DLE требуется)
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Для одиночных предложений тоже показываем детали -->
<div v-else-if="proposal.chains && proposal.chains.length === 1" class="chains-details">
<div class="chains-header">
<strong>Детали цепочки:</strong>
</div>
<div class="chains-list">
<div
v-for="chain in proposal.chains"
:key="chain.chainId"
class="chain-item"
:class="{
'chain-active': Number(chain.state) === 0,
'chain-executed': chain.executed,
'chain-canceled': chain.canceled
}"
>
<div class="chain-main-info">
<span class="chain-name">{{ chain.networkName || `Chain ${chain.chainId}` }}</span>
<span class="chain-status">
<span v-if="chain.executed"> Выполнено</span>
<span v-else-if="chain.canceled"> Отменено</span>
<span v-else-if="chain.state === 5">🟡 Готово к выполнению</span>
<span v-else-if="Number(chain.state) === 0">🟢 Активно</span>
<span v-else> {{ chain.state }}</span>
</span>
</div>
<div class="chain-details-info">
<div class="chain-detail-item">
<span class="detail-label">ID предложения:</span>
<span class="detail-value">#{{ chain.id !== undefined && chain.id !== null ? chain.id : proposal.id }}</span>
</div>
<div class="chain-detail-item">
<span class="detail-label">Голоса:</span>
<span class="detail-value">
👍 {{ chain.forVotes ? (Number(chain.forVotes) / 1e18).toFixed(2) : '0.00' }} DLE |
👎 {{ chain.againstVotes ? (Number(chain.againstVotes) / 1e18).toFixed(2) : '0.00' }} DLE
</span>
</div>
<div class="chain-detail-item">
<span class="detail-label">Кворум:</span>
<span class="detail-value" :class="{ 'quorum-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) >= Number(chain.quorumRequired), 'quorum-not-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) < Number(chain.quorumRequired) }">
{{ chain.forVotes && chain.quorumRequired ?
(Number(chain.forVotes) >= Number(chain.quorumRequired) ? '✅ Достигнут' : '❌ Не достигнут') :
'N/A' }}
({{ chain.quorumRequired ? (Number(chain.quorumRequired) / 1e18).toFixed(2) : '0.00' }} DLE требуется)
</span>
</div>
</div>
</div>
</div>
</div>
@@ -132,7 +242,7 @@
<div class="proposal-actions">
<button
v-if="canVote(proposal)"
v-if="proposal.chains && proposal.chains.length > 1 ? canVoteMultichain(proposal) : canVote(proposal)"
@click="voteOnProposal(proposal.id, true)"
class="btn btn-success"
:disabled="isVoting"
@@ -140,7 +250,7 @@
{{ isVoting ? 'Голосование...' : 'За' }}
</button>
<button
v-if="canVote(proposal)"
v-if="proposal.chains && proposal.chains.length > 1 ? canVoteMultichain(proposal) : canVote(proposal)"
@click="voteOnProposal(proposal.id, false)"
class="btn btn-danger"
:disabled="isVoting"
@@ -148,7 +258,7 @@
{{ isVoting ? 'Голосование...' : 'Против' }}
</button>
<button
v-if="canExecute(proposal)"
v-if="proposal.chains && proposal.chains.length > 1 ? canExecuteMultichain(proposal) : canExecute(proposal)"
@click="executeProposal(proposal.id)"
class="btn btn-primary"
:disabled="isExecuting"
@@ -174,7 +284,7 @@
<script>
import { ref, computed, onMounted, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAuth } from '@/composables/useAuth';
import { useAuthContext } from '@/composables/useAuth';
import { useProposals } from '@/composables/useProposals';
import BaseLayout from '@/components/BaseLayout.vue';
@@ -205,7 +315,7 @@ export default {
setup(props) {
const router = useRouter();
const route = useRoute();
const { currentAddress, address } = useAuth();
const { address } = useAuthContext();
const dleAddress = computed(() => {
return route.query.address;
@@ -230,7 +340,9 @@ export default {
getQuorumPercentage,
getRequiredQuorumPercentage,
canVote,
canVoteMultichain,
canExecute,
canExecuteMultichain,
canCancel
} = useProposals(dleAddress, computed(() => props.isAuthenticated), address);
@@ -278,7 +390,9 @@ export default {
getQuorumPercentage,
getRequiredQuorumPercentage,
canVote,
canVoteMultichain,
canExecute,
canExecuteMultichain,
canCancel
};
}
@@ -601,6 +715,117 @@ export default {
color: #333;
}
.multichain-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-weight: 600;
}
.chains-details {
margin-top: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.chains-header {
margin-bottom: 10px;
color: #333;
font-size: 14px;
}
.chains-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.chain-item {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
background: white;
border-radius: 6px;
border: 1px solid #e9ecef;
font-size: 13px;
margin-bottom: 10px;
}
.chain-item:last-child {
margin-bottom: 0;
}
.chain-item.chain-active {
border-left: 4px solid #28a745;
}
.chain-item.chain-executed {
border-left: 4px solid #007bff;
opacity: 0.7;
}
.chain-item.chain-canceled {
border-left: 4px solid #dc3545;
opacity: 0.7;
}
.chain-main-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.chain-name {
font-weight: 600;
color: #333;
font-size: 14px;
}
.chain-status {
font-size: 12px;
color: #666;
}
.chain-details-info {
display: flex;
flex-direction: column;
gap: 6px;
padding-top: 8px;
border-top: 1px solid #e9ecef;
}
.chain-detail-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.detail-label {
font-weight: 600;
color: #666;
margin-right: 10px;
}
.detail-value {
color: #333;
text-align: right;
flex: 1;
}
.detail-value.quorum-reached {
color: #28a745;
font-weight: 600;
}
.detail-value.quorum-not-reached {
color: #dc3545;
}
@media (max-width: 768px) {
.proposals-page {
padding: 10px;

View File

@@ -218,7 +218,7 @@ import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../components/BaseLayout.vue';
import api from '@/api/axios';
import { ethers } from 'ethers';
import { createProposal } from '@/utils/dle-contract';
import { createProposal, switchToVotingNetwork } from '@/utils/dle-contract';
import { useAuthContext } from '../../composables/useAuth';
// Определяем props
@@ -264,24 +264,57 @@ async function loadDleInfo() {
try {
isLoadingDle.value = true;
// Получаем информацию о DLE из блокчейна
const response = await api.post('/blockchain/read-dle-info', {
dleAddress: dleAddress.value
});
// Получаем информацию о DLE из API, который возвращает все развернутые сети
const response = await api.get('/dle-v2');
if (response.data.success) {
dleInfo.value = response.data.data;
console.log('DLE Info loaded:', dleInfo.value);
const allDles = response.data.data || [];
console.log('All DLEs from API:', allDles);
// Ищем DLE по адресу (может быть в любой из сетей)
let foundDle = null;
for (const dle of allDles) {
// Проверяем, есть ли этот адрес в deployedNetworks
const networkMatch = dle.deployedNetworks?.find(net =>
net.address?.toLowerCase() === dleAddress.value.toLowerCase()
);
if (networkMatch) {
foundDle = dle;
break;
}
}
// Получаем поддерживаемые цепочки из данных DLE
if (dleInfo.value.deployedNetworks && dleInfo.value.deployedNetworks.length > 0) {
supportedChains.value = dleInfo.value.deployedNetworks.map(net => ({
chainId: net.chainId,
name: getChainName(net.chainId)
}));
if (foundDle) {
// Используем deployedNetworks из найденного DLE
dleInfo.value = {
...foundDle,
deployedNetworks: foundDle.deployedNetworks || []
};
console.log('DLE Info loaded:', dleInfo.value);
console.log('Deployed networks count:', dleInfo.value?.deployedNetworks?.length || 0);
console.log('Deployed networks:', dleInfo.value?.deployedNetworks);
// Получаем поддерживаемые цепочки из данных DLE
if (dleInfo.value.deployedNetworks && dleInfo.value.deployedNetworks.length > 0) {
supportedChains.value = dleInfo.value.deployedNetworks.map(net => ({
chainId: net.chainId,
name: getChainName(net.chainId)
}));
} else {
console.warn('No deployed networks found for DLE');
supportedChains.value = [];
}
} else {
console.warn('No deployed networks found for DLE');
supportedChains.value = [];
console.warn('DLE not found in API response, trying blockchain read...');
// Fallback: получаем информацию из блокчейна (только текущая сеть)
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
dleAddress: dleAddress.value
});
if (blockchainResponse.data.success) {
dleInfo.value = blockchainResponse.data.data;
console.log('DLE Info loaded from blockchain:', dleInfo.value);
}
}
}
@@ -334,17 +367,85 @@ function getChainName(chainId) {
return chainNames[chainId] || `Chain ${chainId}`;
}
// Создание encoded call data для _transferTokens
function encodeTransferTokensCall(sender, recipient, amount) {
// Правильный селектор для _transferTokens(address,address,uint256)
// keccak256("_transferTokens(address,address,uint256)")[:4]
const functionSignature = '_transferTokens(address,address,uint256)';
const selectorBytes = ethers.keccak256(ethers.toUtf8Bytes(functionSignature));
const selector = '0x' + selectorBytes.slice(2, 10);
// Функция для проверки, является ли ошибка временной RPC ошибкой
function isRetryableRpcError(error) {
if (!error) return false;
const errorMessage = error.message?.toLowerCase() || '';
const errorCode = error.code;
// Проверяем на временные RPC ошибки
const retryablePatterns = [
'internal json-rpc error',
'json-rpc error',
'rpc error',
'network error',
'timeout',
'connection',
'econnrefused',
'etimedout',
'could not coalesce error',
'rate limit',
'too many requests'
];
// Коды ошибок, которые можно повторить
const retryableCodes = [-32603, -32000, -32002, -32005];
return retryablePatterns.some(pattern => errorMessage.includes(pattern)) ||
retryableCodes.includes(errorCode);
}
// Кодирование параметров
// Функция retry с экспоненциальной задержкой
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Если это не временная RPC ошибка, не повторяем
if (!isRetryableRpcError(error)) {
console.log(`❌ [RETRY] Не повторяемая ошибка:`, error.message);
throw error;
}
// Если это последняя попытка, выбрасываем ошибку
if (attempt === maxRetries) {
console.log(`❌ [RETRY] Исчерпаны все попытки (${maxRetries})`);
throw error;
}
// Вычисляем задержку с экспоненциальным backoff
const delay = initialDelay * Math.pow(2, attempt - 1);
console.log(`🔄 [RETRY] Попытка ${attempt}/${maxRetries} не удалась, повтор через ${delay}ms...`);
console.log(`🔄 [RETRY] Ошибка:`, error.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Создание encoded call data для _transferTokens
// КРИТИЧЕСКИ ВАЖНО: используйте правильную сигнатуру _transferTokens(address,address,uint256)
// и конвертируйте amount в wei
function encodeTransferTokensCall(sender, recipient, amount) {
const functionSignature = '_transferTokens(address,address,uint256)';
const iface = new ethers.Interface([`function ${functionSignature}`]);
const encodedCall = iface.encodeFunctionData('_transferTokens', [sender, recipient, amount]);
// КРИТИЧЕСКИ ВАЖНО: конвертируем amount в wei (1 токен = 10^18 wei)
const amountInWei = ethers.parseUnits(amount.toString(), 18);
// Кодирование операции с тремя параметрами: sender, recipient, amountInWei
const encodedCall = iface.encodeFunctionData('_transferTokens', [
sender, // адрес инициатора (обязательно!)
recipient, // адрес получателя
amountInWei // количество в wei (обязательно!)
]);
return encodedCall;
}
@@ -360,8 +461,8 @@ async function submitForm() {
throw new Error('Некорректный адрес отправителя');
}
// Проверяем, что адрес отправителя совпадает с адресом пользователя
if (formData.value.sender !== currentUserAddress.value) {
// Проверяем, что адрес отправителя совпадает с адресом пользователя (case-insensitive)
if (formData.value.sender.toLowerCase() !== currentUserAddress.value?.toLowerCase()) {
throw new Error('Адрес отправителя должен совпадать с вашим подключенным кошельком');
}
@@ -369,6 +470,16 @@ async function submitForm() {
throw new Error('Некорректный адрес получателя');
}
// Проверяем, что получатель не является zero address
if (formData.value.recipient.toLowerCase() === '0x0000000000000000000000000000000000000000') {
throw new Error('Адрес получателя не может быть нулевым адресом');
}
// Проверяем, что отправитель и получатель не совпадают
if (formData.value.sender.toLowerCase() === formData.value.recipient.toLowerCase()) {
throw new Error('Адрес отправителя и получателя не могут совпадать');
}
if (!formData.value.amount || formData.value.amount <= 0) {
throw new Error('Некорректное количество токенов');
}
@@ -381,23 +492,73 @@ async function submitForm() {
throw new Error('Выберите время голосования');
}
// Создание encoded call data для передачи токенов
const transferCallData = encodeTransferTokensCall(
formData.value.sender,
formData.value.recipient,
formData.value.amount
);
// Получаем все поддерживаемые цепочки из DLE информации
const allChains = dleInfo.value?.deployedNetworks
? dleInfo.value.deployedNetworks.map(net => net.chainId)
: [];
console.log('DLE Info for proposal creation:', dleInfo.value);
console.log('Deployed networks:', dleInfo.value?.deployedNetworks);
if (!dleInfo.value?.deployedNetworks || dleInfo.value.deployedNetworks.length === 0) {
throw new Error('Не найдены развернутые сети для DLE контракта');
}
const allChains = dleInfo.value.deployedNetworks.map(net => {
console.log('Network info:', { chainId: net.chainId, address: net.address, name: net.networkName });
return net.chainId;
});
console.log('Creating proposals in chains:', allChains);
console.log('Number of chains:', allChains.length);
if (allChains.length === 0) {
throw new Error('Не найдено ни одной цепочки для создания предложений');
}
// Создаем предложения последовательно во всех цепочках с переключением сети
console.log(`🚀 Starting to create ${allChains.length} proposals sequentially...`);
const results = [];
for (let index = 0; index < allChains.length; index++) {
const chainId = allChains[index];
console.log(`📝 [${index + 1}/${allChains.length}] Starting proposal creation for chain ${chainId}`);
// Создаем предложения параллельно во всех цепочках
const proposalPromises = allChains.map(async (chainId) => {
try {
// Переключаемся на нужную сеть перед созданием предложения
console.log(`🔄 [${index + 1}/${allChains.length}] Switching to network ${chainId}...`);
const networkSwitched = await switchToVotingNetwork(chainId);
console.log(`🔄 [${index + 1}/${allChains.length}] Network switch result:`, networkSwitched);
if (!networkSwitched) {
throw new Error(`Не удалось переключиться на сеть ${chainId}`);
}
// Проверяем текущую сеть после переключения
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
console.log(`🔍 [${index + 1}/${allChains.length}] Current chain after switch:`, currentChainId, `Expected: 0x${chainId.toString(16)}`);
// Небольшая задержка после переключения сети
console.log(`⏳ [${index + 1}/${allChains.length}] Waiting 1 second after network switch...`);
await new Promise(resolve => setTimeout(resolve, 1000));
// КРИТИЧЕСКИ ВАЖНО: Получаем адрес signer для текущей сети
// Это гарантирует, что sender в операции совпадает с инициатором предложения
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const senderAddress = await signer.getAddress();
console.log(`🔑 [${index + 1}/${allChains.length}] Sender address for chain ${chainId}:`, senderAddress);
// Проверяем, что адрес signer совпадает с адресом из формы
if (senderAddress.toLowerCase() !== formData.value.sender.toLowerCase()) {
throw new Error(`Адрес signer (${senderAddress}) не совпадает с адресом отправителя из формы (${formData.value.sender})`);
}
// Кодируем операцию перевода токенов для текущей сети
// Используем адрес signer, чтобы гарантировать совпадение с инициатором предложения
const transferCallData = encodeTransferTokensCall(
senderAddress,
formData.value.recipient,
formData.value.amount
);
const proposalData = {
description: formData.value.description,
duration: parseInt(formData.value.votingDuration),
@@ -406,38 +567,68 @@ async function submitForm() {
timelockDelay: 0
};
console.log(`Creating proposal in chain ${chainId}:`, proposalData);
console.log(`📋 [${index + 1}/${allChains.length}] Proposal data for chain ${chainId}:`, proposalData);
// Получаем адрес контракта для этой цепочки
const networkInfo = dleInfo.value?.deployedNetworks?.find(net => net.chainId === chainId);
const contractAddress = networkInfo?.address || dleAddress.value;
const result = await createProposal(contractAddress, proposalData);
console.log(`🔄 [${index + 1}/${allChains.length}] Calling createProposal for chain ${chainId}, contract: ${contractAddress}`);
// Используем retry для временных RPC ошибок
const result = await retryWithBackoff(
async () => {
return await createProposal(contractAddress, proposalData);
},
3, // Максимум 3 попытки
2000 // Начальная задержка 2 секунды
);
console.log(`✅ [${index + 1}/${allChains.length}] Proposal created successfully in chain ${chainId}:`, result);
return {
// Дополнительная задержка после подтверждения транзакции
// чтобы MetaMask успел обработать транзакцию перед переходом к следующей цепочке
// Для Base Sepolia увеличиваем задержку, так как уведомления могут приходить медленнее
if (result.success && result.txHash) {
const delay = chainId === 84532 ? 5000 : 3000; // 5 секунд для Base Sepolia, 3 для остальных
console.log(`⏳ [${index + 1}/${allChains.length}] Waiting ${delay/1000} seconds for MetaMask to process transaction in ${getChainName(chainId)}...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
results.push({
chainId,
success: result.success,
proposalId: result.proposalId,
txHash: result.txHash,
error: result.error,
contractAddress
};
});
} catch (error) {
console.error(`Error creating proposal in chain ${chainId}:`, error);
return {
console.error(`❌ [${index + 1}/${allChains.length}] Error creating proposal in chain ${chainId}:`, error);
console.error(`❌ [${index + 1}/${allChains.length}] Error details:`, {
message: error.message,
stack: error.stack,
name: error.name
});
results.push({
chainId,
success: false,
error: error.message,
contractAddress: dleAddress.value
};
error: error.message || 'Неизвестная ошибка',
contractAddress: dleInfo.value?.deployedNetworks?.find(net => net.chainId === chainId)?.address || dleAddress.value
});
}
});
}
const results = await Promise.all(proposalPromises);
console.log(`📊 Всего обработано цепочек: ${results.length} из ${allChains.length}`);
console.log(`📊 Результаты создания предложений:`, results);
// Проверяем результаты
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
console.log(`✅ Успешно создано в ${successful.length} цепочках`);
console.log(`❌ Ошибок в ${failed.length} цепочках`);
if (successful.length > 0) {
proposalResult.value = {
success: true,
@@ -447,6 +638,10 @@ async function submitForm() {
failedChains: failed
};
// Автоматический переход на страницу предложений
console.log('🔄 Переход на страницу предложений...');
router.push(`/management/proposals?address=${dleAddress.value}`);
// Очистка формы только при полном успехе
if (failed.length === 0) {
formData.value = {