feat: Добавлены формы деплоя модулей DLE с полными настройками

- Создана форма деплоя TreasuryModule с детальными настройками казны
- Создана форма деплоя TimelockModule с настройками временных задержек
- Создана форма деплоя DLEReader с простой конфигурацией
- Добавлены маршруты и индексы для всех модулей
- Исправлены пути импорта BaseLayout
- Добавлены авторские права во все файлы
- Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
2025-09-23 02:57:59 +03:00
parent 9f94295d15
commit de0f8aecf2
63 changed files with 11631 additions and 1920 deletions

View File

@@ -70,7 +70,7 @@
>
<div class="proposal-header">
<h5>{{ proposal.description || 'Без описания' }}</h5>
<h5>{{ getProposalTitle(proposal) }}</h5>
<span class="proposal-status" :class="proposal.status">
{{ getProposalStatusText(proposal.status) }}
</span>
@@ -92,6 +92,27 @@
<div class="detail-item">
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
</div>
<!-- Детальная информация о модуле -->
<div v-if="proposal.decodedData" class="module-details">
<div class="detail-item">
<strong>Тип модуля:</strong> {{ getModuleName(proposal.decodedData.moduleId) }}
</div>
<div class="detail-item">
<strong>Адрес модуля:</strong>
<a :href="getEtherscanUrl(proposal.decodedData.moduleAddress, proposal.decodedData.chainId)"
target="_blank" class="address-link">
{{ shortenAddress(proposal.decodedData.moduleAddress) }}
</a>
</div>
<div class="detail-item">
<strong>Сеть:</strong> {{ getChainName(proposal.decodedData.chainId) }}
</div>
<div class="detail-item">
<strong>Длительность:</strong> {{ formatDuration(proposal.decodedData.duration) }}
</div>
</div>
<div class="detail-item">
<strong>Голоса:</strong>
<div class="votes-container">
@@ -100,7 +121,7 @@
<span class="against">Против: {{ formatVotes(proposal.againstVotes) }}</span>
</div>
<div class="quorum-info">
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum() }}%</span>
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum(proposal) }}%</span>
</div>
<div class="quorum-progress">
<div class="progress-bar">
@@ -140,13 +161,21 @@
<i class="fas fa-times"></i> Против
</button>
<button
v-if="canExecute(proposal) && props.isAuthenticated && hasAdminRights()"
v-if="canExecute(proposal) && props.isAuthenticated"
class="btn btn-sm btn-primary"
@click="executeProposalLocal(proposal.id)"
>
<i class="fas fa-play"></i> Исполнить
</button>
<!-- Информация для не-инициаторов -->
<div v-else-if="proposal.state === 5 && !proposal.executed && props.isAuthenticated" class="execution-notice">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Только инициатор предложения может его исполнить
</small>
</div>
<!-- Информация для неавторизованных пользователей -->
<div v-if="!props.isAuthenticated" class="auth-notice">
<small class="text-muted">
@@ -533,7 +562,7 @@ import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '../../composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI } from '../../services/proposalsService.js';
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
import api from '../../api/axios';
const showTargetChains = computed(() => {
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
@@ -543,6 +572,306 @@ const showTargetChains = computed(() => {
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
// Best Practice: WebSocket-based подписка на обновления голосования
function subscribeToVoteUpdates(txHash, proposalId, actionType) {
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для:', { txHash, proposalId, actionType });
// Создаем уникальный обработчик для этой транзакции
const voteHandler = (data) => {
console.log('[DleProposalsView] Получено WebSocket уведомление о голосовании:', data);
// Проверяем, что это наша транзакция
if (data.txHash === txHash || data.proposalId === proposalId) {
console.log('[DleProposalsView] Найдено совпадение транзакции, обновляем данные');
// Обновляем данные
loadDleData().then(() => {
// Показываем успешное уведомление
showSuccessNotification(txHash, actionType);
});
// Отписываемся от уведомлений
wsClient.off('proposal_voted', voteHandler);
}
};
// Подписываемся на уведомления о голосовании
wsClient.on('proposal_voted', voteHandler);
// Устанавливаем таймаут на случай, если WebSocket не сработает
setTimeout(() => {
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений, отписываемся');
wsClient.off('proposal_voted', voteHandler);
// Fallback: обновляем данные в любом случае
loadDleData().then(() => {
showTimeoutNotification(txHash, actionType);
});
}, 60000); // 60 секунд таймаут
}
// WebSocket-based подписка на обновления исполнения
function subscribeToExecutionUpdates(txHash, proposalId) {
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для исполнения:', { txHash, proposalId });
// Создаем уникальный обработчик для этой транзакции
const executionHandler = (data) => {
console.log('[DleProposalsView] Получено WebSocket уведомление об исполнении:', data);
// Проверяем, что это наша транзакция
if (data.txHash === txHash || data.proposalId === proposalId) {
console.log('[DleProposalsView] Найдено совпадение транзакции исполнения, обновляем данные');
// Обновляем данные
loadDleData().then(() => {
// Показываем успешное уведомление
showSuccessNotification(txHash, 'execution');
});
// Отписываемся от уведомлений
wsClient.off('proposal_executed', executionHandler);
}
};
// Подписываемся на уведомления об исполнении
wsClient.on('proposal_executed', executionHandler);
// Устанавливаем таймаут на случай, если WebSocket не сработает
setTimeout(() => {
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений об исполнении, отписываемся');
wsClient.off('proposal_executed', executionHandler);
// Fallback: обновляем данные в любом случае
loadDleData().then(() => {
showTimeoutNotification(txHash, 'execution');
});
}, 60000); // 60 секунд таймаут
}
// Функция для отслеживания транзакции исполнения на backend
async function trackExecutionTransaction(txHash, dleAddress, proposalId) {
try {
console.log('[DleProposalsView] Запускаем отслеживание транзакции исполнения на backend:', { txHash, dleAddress, proposalId });
const response = await api.post('/dle-proposals/track-execution-transaction', {
txHash: txHash,
dleAddress: dleAddress,
proposalId: proposalId
});
if (response.data.success) {
console.log('[DleProposalsView] Backend подтвердил транзакцию исполнения:', response.data);
} else {
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию исполнения:', response.data.error);
}
} catch (error) {
console.error('[DleProposalsView] Ошибка при отслеживании транзакции исполнения на backend:', error);
}
}
// Функция для отслеживания транзакции голосования на backend
async function trackVoteTransaction(txHash, dleAddress, proposalId, support) {
try {
console.log('[DleProposalsView] Запускаем отслеживание транзакции на backend:', { txHash, dleAddress, proposalId, support });
const response = await api.post('/dle-proposals/track-vote-transaction', {
txHash: txHash,
dleAddress: dleAddress,
proposalId: proposalId,
support: support
});
if (response.data.success) {
console.log('[DleProposalsView] Backend подтвердил транзакцию:', response.data);
} else {
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию:', response.data.error);
}
} catch (error) {
console.error('[DleProposalsView] Ошибка при отслеживании транзакции на backend:', error);
}
}
// Показ уведомления о транзакции
function showTransactionNotification(txHash, message) {
// Создаем уведомление с ссылкой на Etherscan
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
// Можно использовать toast-библиотеку или создать кастомное уведомление
const notification = document.createElement('div');
notification.className = 'transaction-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">⏳</span>
<span class="notification-title">${message}</span>
</div>
<div class="notification-body">
<p>Ожидаем подтверждения транзакции...</p>
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
// Добавляем стили
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
// Автоматически удаляем через 10 секунд
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 10000);
}
// Показ успешного уведомления
function showSuccessNotification(txHash, actionType) {
const actionText = actionType === 'vote' ? 'Голосование подтверждено!' : 'Голосование "против" подтверждено!';
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
const notification = document.createElement('div');
notification.className = 'success-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">✅</span>
<span class="notification-title">${actionText}</span>
</div>
<div class="notification-body">
<p>Данные обновлены</p>
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 5000);
}
// Показ уведомления об ошибке
function showErrorNotification(txHash, message) {
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
const notification = document.createElement('div');
notification.className = 'error-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">❌</span>
<span class="notification-title">${message}</span>
</div>
<div class="notification-body">
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 8000);
}
// Показ уведомления о таймауте
function showTimeoutNotification(txHash, actionType) {
const actionText = actionType === 'vote' ? 'Голосование' : 'Голосование "против"';
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
const notification = document.createElement('div');
notification.className = 'timeout-notification';
notification.innerHTML = `
<div class="notification-content">
<div class="notification-header">
<span class="notification-icon">⏰</span>
<span class="notification-title">${actionText} отправлено</span>
</div>
<div class="notification-body">
<p>Подтверждение не получено, но данные обновлены</p>
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
Посмотреть в Etherscan
</a>
</div>
</div>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 8000);
}
const props = defineProps({
dleAddress: { type: String, required: false, default: null },
dleContract: { type: Object, required: false, default: null },
@@ -670,15 +999,30 @@ async function loadDleData() {
console.log('[Frontend] Массив предложений:', proposalsData);
// Преобразуем данные из API в формат для frontend
proposals.value = proposalsData.map(proposal => {
proposals.value = await Promise.all(proposalsData.map(async (proposal) => {
const transformedProposal = {
...proposal,
status: getProposalStatus(proposal),
deadline: proposal.deadline || 0
};
// Если есть transactionHash, декодируем данные предложения
if (proposal.transactionHash) {
try {
console.log('[Frontend] Декодируем данные предложения:', proposal.transactionHash);
const decodedData = await decodeProposalData(proposal.transactionHash);
if (decodedData.success) {
transformedProposal.decodedData = decodedData.data;
console.log('[Frontend] Декодированные данные:', decodedData.data);
}
} catch (error) {
console.error('[Frontend] Ошибка декодирования данных предложения:', error);
}
}
console.log('[Frontend] Преобразованное предложение:', transformedProposal);
return transformedProposal;
});
}));
console.log('[Frontend] Итоговый список предложений:', proposals.value);
@@ -819,8 +1163,13 @@ function getProposalStatus(proposal) {
const forVotes = Number(proposal.forVotes) || 0;
const againstVotes = Number(proposal.againstVotes) || 0;
// Если есть голоса, определяем результат
if (forVotes > 0 || againstVotes > 0) {
// Проверяем, достигнут ли кворум
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum(proposal);
const quorumReached = quorumPercentage >= requiredQuorum;
// Если есть голоса И кворум достигнут, определяем результат
if ((forVotes > 0 || againstVotes > 0) && quorumReached) {
if (forVotes > againstVotes) {
return 'succeeded';
} else if (againstVotes > forVotes) {
@@ -828,6 +1177,7 @@ function getProposalStatus(proposal) {
}
}
// Если кворум не достигнут или нет голосов, предложение активно
return 'active';
}
@@ -843,6 +1193,61 @@ function getProposalStatusText(status) {
return statusMap[status] || status;
}
function getProposalTitle(proposal) {
// Если есть декодированные данные, показываем детальную информацию
if (proposal.decodedData) {
const { moduleId, moduleAddress, chainId, duration } = proposal.decodedData;
// Декодируем moduleId из hex в строку
let moduleName = 'Неизвестный модуль';
try {
moduleName = ethers.toUtf8String(moduleId).replace(/\0/g, '');
} catch (e) {
console.log('Не удалось декодировать moduleId:', moduleId);
}
return `Добавить модуль: ${moduleName}`;
}
// Иначе показываем обычное описание
return proposal.description || 'Без описания';
}
function getModuleName(moduleId) {
try {
return ethers.toUtf8String(moduleId).replace(/\0/g, '');
} catch (e) {
return 'Неизвестный модуль';
}
}
function getEtherscanUrl(address, chainId) {
const chainMap = {
1: 'https://etherscan.io',
11155111: 'https://sepolia.etherscan.io',
17000: 'https://holesky.etherscan.io',
421614: 'https://sepolia.arbiscan.io',
84532: 'https://sepolia.basescan.org'
};
const baseUrl = chainMap[chainId] || 'https://etherscan.io';
return `${baseUrl}/address/${address}`;
}
function formatDuration(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days} дн. ${hours} ч.`;
} else if (hours > 0) {
return `${hours} ч. ${minutes} мин.`;
} else {
return `${minutes} мин.`;
}
}
function getProposalStatusClass(status) {
const classMap = {
'pending': 'status-pending',
@@ -901,16 +1306,25 @@ function getQuorumPercentage(proposal) {
function getQuorumProgress(proposal) {
const percentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const requiredQuorum = getRequiredQuorum(proposal);
const progress = Math.min((percentage / requiredQuorum) * 100, 100);
console.log('[Quorum] Прогресс кворума:', { percentage, requiredQuorum, progress });
return progress;
}
function getRequiredQuorum() {
function getRequiredQuorum(proposal = null) {
// Если есть данные о предложении с quorumRequired, используем их
if (proposal && proposal.quorumRequired && selectedDle.value?.totalSupply) {
const totalSupplyWei = parseFloat(selectedDle.value.totalSupply) * Math.pow(10, 18);
const quorumPercentage = (proposal.quorumRequired / totalSupplyWei) * 100;
console.log('[Quorum] Требуемый кворум из предложения:', quorumPercentage, 'quorumRequired:', proposal.quorumRequired, 'totalSupply:', totalSupplyWei);
return Math.round(quorumPercentage * 100) / 100;
}
// Fallback к данным DLE
const quorum = selectedDle.value?.quorumPercentage || 51;
console.log('[Quorum] Требуемый кворум из DLE:', quorum, 'DLE данные:', selectedDle.value);
return quorum; // По умолчанию 51% если данные не загружены
return quorum;
}
function formatVotes(votes) {
@@ -974,15 +1388,17 @@ function canExecute(proposal) {
const now = Math.floor(Date.now() / 1000);
const deadline = proposal.deadline || 0;
// Предложение можно выполнить только если:
// 1. Дедлайн истек
// 2. Кворум достигнут
// 3. Предложение еще не выполнено
// Предложение можно выполнить если:
// 1. Кворум достигнут ИЛИ предложение уже принято (state: 5)
// 2. Предложение еще не выполнено
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const requiredQuorum = getRequiredQuorum(proposal);
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
const deadlinePassed = deadline > 0 && now >= deadline;
// Если предложение уже принято (state: 5), можно исполнять
const isProposalPassed = proposal.state === 5 || proposal.isPassed === true;
// Добавляем отладочную информацию
console.log('[canExecute] Проверка предложения:', {
proposalId: proposal.id,
@@ -992,10 +1408,29 @@ function canExecute(proposal) {
deadline,
now,
deadlinePassed,
executed: proposal.executed
executed: proposal.executed,
state: proposal.state,
isPassed: proposal.isPassed,
isProposalPassed
});
return deadlinePassed && hasReachedQuorum && !proposal.executed;
// Проверяем, что текущий пользователь - инициатор предложения
const isInitiator = address.value && proposal.initiator &&
address.value.toLowerCase() === proposal.initiator.toLowerCase();
console.log('[canExecute] Проверка инициатора:', {
currentAddress: address.value,
proposalInitiator: proposal.initiator,
isInitiator
});
// Можно исполнять если:
// 1. (кворум достигнут И дедлайн истек) ИЛИ предложение уже принято
// 2. Пользователь - инициатор предложения
// 3. Предложение не выполнено
return ((hasReachedQuorum && deadlinePassed) || isProposalPassed) &&
isInitiator &&
!proposal.executed;
}
function hasSigned(proposalId) {
@@ -1191,10 +1626,107 @@ async function signProposalLocal(proposalId) {
console.log('[Debug] Попытка подписи для предложения:', proposalId);
console.log('[Debug] Адрес кошелька:', address.value);
await voteForProposalAPI(dleAddress.value, proposalId, true); // Подпись = голос "за"
// Получаем данные транзакции от backend
const result = await voteForProposalAPI(dleAddress.value, proposalId, true);
await loadDleData();
alert('✅ Предложение подписано!');
if (result.success) {
console.log('[DleProposalsView] Данные транзакции голосования получены:', result);
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем, что есть подключенный аккаунт
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
}
if (!accounts || accounts.length === 0) {
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
}
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
// Проверяем подключение к правильной сети
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0xaa36a7'; // Sepolia
if (chainId !== expectedChainId) {
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
try {
// Пытаемся переключиться на Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
console.log('[DleProposalsView] Успешно переключились на Sepolia');
} catch (switchError) {
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log('[DleProposalsView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[DleProposalsView] Отправляем транзакцию голосования:', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[DleProposalsView] Транзакция голосования отправлена:', txHash);
// Показываем уведомление с возможностью отслеживания
showTransactionNotification(txHash, 'Голосование отправлено!');
// Подписываемся на WebSocket уведомления о голосовании
subscribeToVoteUpdates(txHash, proposalId, 'vote');
// Запускаем отслеживание транзакции на backend
trackVoteTransaction(txHash, dleAddress.value, proposalId, true);
} catch (txError) {
console.error('[DleProposalsView] Ошибка отправки транзакции голосования:', txError);
alert('❌ Ошибка отправки транзакции голосования: ' + txError.message);
}
} else {
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
console.error('Ошибка при подписании:', error);
@@ -1225,10 +1757,107 @@ async function cancelSignatureLocal(proposalId) {
console.log('[Debug] Попытка голосования "против" для предложения:', proposalId);
console.log('[Debug] Адрес кошелька:', address.value);
await voteForProposalAPI(dleAddress.value, proposalId, false); // Голос "против"
// Получаем данные транзакции от backend
const result = await voteForProposalAPI(dleAddress.value, proposalId, false);
await loadDleData();
alert('✅ Ваш голос "против" учтен!');
if (result.success) {
console.log('[DleProposalsView] Данные транзакции голосования "против" получены:', result);
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем, что есть подключенный аккаунт
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
}
if (!accounts || accounts.length === 0) {
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
}
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
// Проверяем подключение к правильной сети
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0xaa36a7'; // Sepolia
if (chainId !== expectedChainId) {
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
try {
// Пытаемся переключиться на Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
console.log('[DleProposalsView] Успешно переключились на Sepolia');
} catch (switchError) {
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log('[DleProposalsView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[DleProposalsView] Отправляем транзакцию голосования "против":', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[DleProposalsView] Транзакция голосования "против" отправлена:', txHash);
// Показываем уведомление с возможностью отслеживания
showTransactionNotification(txHash, 'Голосование "против" отправлено!');
// Подписываемся на WebSocket уведомления о голосовании
subscribeToVoteUpdates(txHash, proposalId, 'vote-against');
// Запускаем отслеживание транзакции на backend
trackVoteTransaction(txHash, dleAddress.value, proposalId, false);
} catch (txError) {
console.error('[DleProposalsView] Ошибка отправки транзакции голосования "против":', txError);
alert('❌ Ошибка отправки транзакции голосования "против": ' + txError.message);
}
} else {
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
console.error('Ошибка при голосовании "против":', error);
@@ -1243,23 +1872,131 @@ async function cancelSignatureLocal(proposalId) {
// Исполнение предложения
async function executeProposalLocal(proposalId) {
// Проверка прав админа для исполнения
// Проверка авторизации
if (!props.isAuthenticated) {
alert('❌ Для исполнения предложений необходимо авторизоваться в приложении');
return;
}
// Дополнительная проверка на права админа
if (!hasAdminRights()) {
alert('❌ Для исполнения предложений необходимы права администратора');
// Проверка, что пользователь - инициатор предложения
const proposal = proposals.value.find(p => p.id === proposalId);
if (!proposal) {
alert('❌ Предложение не найдено');
return;
}
const isInitiator = address.value && proposal.initiator &&
address.value.toLowerCase() === proposal.initiator.toLowerCase();
if (!isInitiator) {
alert('❌ Только инициатор предложения может его исполнить');
return;
}
try {
await executeProposalAPI(dleAddress.value, proposalId);
console.log('[Debug] Попытка исполнения предложения:', proposalId);
console.log('[Debug] Адрес кошелька:', address.value);
await loadDleData();
alert('✅ Предложение успешно исполнено!');
// Получаем данные транзакции от backend
const result = await executeProposalAPI(dleAddress.value, proposalId);
if (result.success) {
console.log('[DleProposalsView] Данные транзакции исполнения получены:', result);
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем подключение к MetaMask
if (!window.ethereum) {
throw new Error('MetaMask не установлен');
}
// Запрашиваем разрешение на подключение к MetaMask
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
if (accounts.length === 0) {
throw new Error('Нет подключенных аккаунтов в MetaMask');
}
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
// Проверяем сеть
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0xaa36a7'; // Sepolia (11155111)
if (chainId !== expectedChainId) {
console.log(`[DleProposalsView] Неправильная сеть! Текущая: ${chainId}, ожидается: ${expectedChainId}`);
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
} catch (switchError) {
if (switchError.code === 4902) {
console.log('[DleProposalsView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[DleProposalsView] Отправляем транзакцию исполнения:', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[DleProposalsView] Транзакция исполнения отправлена:', txHash);
// Показываем уведомление с возможностью отслеживания
showTransactionNotification(txHash, 'Исполнение предложения отправлено!');
// Подписываемся на WebSocket уведомления о исполнении
subscribeToExecutionUpdates(txHash, proposalId);
// Запускаем отслеживание транзакции на backend
trackExecutionTransaction(txHash, dleAddress.value, proposalId);
} catch (txError) {
console.error('[DleProposalsView] Ошибка отправки транзакции исполнения:', txError);
alert('❌ Ошибка отправки транзакции исполнения: ' + txError.message);
}
} else {
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
console.error('Ошибка при исполнении предложения:', error);
@@ -1764,6 +2501,14 @@ onUnmounted(() => {
color: #721c24;
}
.execution-notice {
margin-top: 8px;
padding: 8px 12px;
background: #e2e3e5;
border-radius: 4px;
border-left: 3px solid #6c757d;
}
.proposal-status.canceled {
background: #fff3cd;
color: #856404;
@@ -1778,6 +2523,28 @@ onUnmounted(() => {
font-size: 0.9rem;
}
.module-details {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
}
.module-details .detail-item {
margin-bottom: 0.75rem;
}
.address-link {
color: #007bff;
text-decoration: none;
font-family: monospace;
}
.address-link:hover {
text-decoration: underline;
}
.votes-container {
display: flex;
flex-direction: column;

View File

@@ -103,6 +103,29 @@
</div>
</div>
<!-- DLEReader -->
<div class="module-deploy-card">
<div class="module-content">
<h4>DLEReader</h4>
<p>Чтение данных DLE - API для получения информации о контракте и предложениях</p>
<div class="module-features">
<span class="feature-tag">API</span>
<span class="feature-tag">Чтение</span>
<span class="feature-tag">Данные</span>
<span class="feature-tag">Интеграция</span>
</div>
</div>
<div class="module-actions">
<button
class="btn btn-primary btn-deploy"
@click="router.push(`/management/modules/deploy/reader?address=${route.query.address}`)"
>
<i class="fas fa-rocket"></i>
Деплой
</button>
</div>
</div>
<!-- CommunicationModule -->
<div class="module-deploy-card">
<div class="module-content">
@@ -465,12 +488,44 @@
<div class="modules-list">
<div class="list-header">
<h3>📋 Модули DLE</h3>
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules">
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules }"></i> Обновить
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules || isLoadingDeploymentStatus">
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules || isLoadingDeploymentStatus }"></i> Обновить
</button>
</div>
<div v-if="isLoadingModules" class="loading-modules">
<!-- Статус деплоя -->
<div v-if="isLoadingDeploymentStatus" class="deployment-status">
<div class="status-loading">
<i class="fas fa-spinner fa-spin"></i>
<span>Проверка статуса деплоя...</span>
</div>
</div>
<div v-else-if="!canShowModules" class="deployment-status">
<div class="status-message" :class="deploymentStatus">
<div class="status-icon">
<i v-if="deploymentStatus === 'completed'" class="fas fa-check-circle"></i>
<i v-else-if="deploymentStatus === 'in_progress'" class="fas fa-spinner fa-spin"></i>
<i v-else-if="deploymentStatus === 'failed'" class="fas fa-exclamation-triangle"></i>
<i v-else-if="deploymentStatus === 'not_started'" class="fas fa-play-circle"></i>
<i v-else class="fas fa-question-circle"></i>
</div>
<div class="status-content">
<h4>{{ deploymentStatusMessage }}</h4>
<p v-if="deploymentStatus === 'not_started'">
Для активации модулей необходимо запустить поэтапный деплой DLE.
</p>
<p v-else-if="deploymentStatus === 'failed'">
Проверьте логи деплоя и повторите попытку через форму деплоя.
</p>
<p v-else-if="deploymentStatus === 'in_progress'">
Дождитесь завершения деплоя. Модули станут доступны автоматически.
</p>
</div>
</div>
</div>
<div v-else-if="isLoadingModules" class="loading-modules">
<p>Загрузка модулей...</p>
</div>
@@ -479,7 +534,7 @@
<p>Используйте форму выше для добавления первого модуля</p>
</div>
<div v-else class="modules-grid">
<div v-else-if="canShowModules && modules.length > 0" class="modules-grid">
<div
v-for="module in modules"
:key="module.moduleId"
@@ -590,7 +645,9 @@ import {
createRemoveModuleProposal,
isModuleActive,
getModuleAddress,
getAllModules
getAllModules,
getNetworksInfo,
getDeploymentStatus
} from '../../services/modulesService.js';
import api from '../../api/axios';
@@ -612,11 +669,16 @@ const route = useRoute();
const selectedDle = ref(null);
const isLoadingDle = ref(false);
const modules = ref([]);
const supportedNetworks = ref([]);
const isLoadingModules = ref(false);
const isCreating = ref(false);
const isRemoving = ref(null);
const isActivating = ref(null);
const isVerifying = ref(null);
// Состояние деплоя
const deploymentStatus = ref('unknown'); // 'unknown', 'completed', 'in_progress', 'failed', 'not_started'
const isLoadingDeploymentStatus = ref(false);
const lastUpdateTime = ref('');
// Форма нового модуля
@@ -641,6 +703,23 @@ const modulesCount = computed(() => modules.value.length);
const activeModulesCount = computed(() => modules.value.filter(m => m.isActive).length);
const inactiveModulesCount = computed(() => modules.value.filter(m => !m.isActive).length);
// Статус деплоя
const canShowModules = computed(() => deploymentStatus.value === 'completed');
const deploymentStatusMessage = computed(() => {
switch (deploymentStatus.value) {
case 'completed':
return 'Деплой завершен. Модули готовы к использованию.';
case 'in_progress':
return 'Деплой в процессе. Модули будут доступны после завершения.';
case 'failed':
return 'Деплой не удался. Проверьте логи и повторите попытку.';
case 'not_started':
return 'Деплой не начат. Запустите деплой для активации модулей.';
default:
return 'Статус деплоя неизвестен. Проверьте состояние системы.';
}
});
// Загрузка данных DLE
async function loadDleData() {
try {
@@ -672,6 +751,37 @@ async function loadDleData() {
}
}
// Проверка статуса деплоя
async function checkDeploymentStatus() {
try {
isLoadingDeploymentStatus.value = true;
const dleAddress = route.query.address;
if (!dleAddress) {
console.warn('[ModulesView] Адрес DLE не найден для проверки статуса деплоя');
deploymentStatus.value = 'unknown';
return;
}
console.log('[ModulesView] Проверка статуса деплоя для DLE:', dleAddress);
const statusResponse = await getDeploymentStatus(dleAddress);
console.log('[ModulesView] Статус деплоя:', statusResponse);
if (statusResponse.success) {
deploymentStatus.value = statusResponse.data.status || 'unknown';
} else {
deploymentStatus.value = 'unknown';
}
} catch (error) {
console.error('[ModulesView] Ошибка при проверке статуса деплоя:', error);
deploymentStatus.value = 'unknown';
} finally {
isLoadingDeploymentStatus.value = false;
}
}
// Загрузка модулей
async function loadModules() {
try {
@@ -681,15 +791,30 @@ async function loadModules() {
if (!dleAddress) {
console.error('[ModulesView] Адрес DLE не указан');
modules.value = [];
supportedNetworks.value = [];
return;
}
console.log('[ModulesView] Загрузка модулей для DLE:', dleAddress);
// Загружаем модули через modulesService
const modulesResponse = await getAllModules(dleAddress);
// Сначала проверяем статус деплоя
await checkDeploymentStatus();
console.log('[ModulesView] Ответ от API:', modulesResponse);
// Если деплой не завершен, не загружаем модули
if (deploymentStatus.value !== 'completed') {
console.log('[ModulesView] Деплой не завершен, модули не загружаются. Статус:', deploymentStatus.value);
modules.value = [];
return;
}
// Загружаем модули и информацию о сетях параллельно
const [modulesResponse, networksResponse] = await Promise.all([
getAllModules(dleAddress),
getNetworksInfo(dleAddress)
]);
console.log('[ModulesView] Ответ от API модулей:', modulesResponse);
console.log('[ModulesView] Ответ от API сетей:', networksResponse);
if (modulesResponse.success) {
modules.value = modulesResponse.data.modules || [];
@@ -697,7 +822,7 @@ async function loadModules() {
count: modules.value.length,
modules: modules.value.map(m => ({
name: m.moduleName,
address: m.moduleAddress,
addresses: m.addresses?.length || 0,
active: m.isActive,
id: m.moduleId
})),
@@ -717,6 +842,20 @@ async function loadModules() {
console.error('[ModulesView] Ошибка загрузки модулей:', modulesResponse.error);
modules.value = [];
}
if (networksResponse.success) {
supportedNetworks.value = networksResponse.data.networks || [];
console.log('[ModulesView] Сети загружены успешно:', {
count: supportedNetworks.value.length,
networks: supportedNetworks.value.map(n => ({
name: n.networkName,
chainId: n.chainId
}))
});
} else {
console.error('[ModulesView] Ошибка загрузки сетей:', networksResponse.error);
supportedNetworks.value = [];
}
} catch (error) {
console.error('[ModulesView] Ошибка загрузки модулей:', error);
@@ -726,6 +865,7 @@ async function loadModules() {
status: error.response?.status
});
modules.value = [];
supportedNetworks.value = [];
} finally {
isLoadingModules.value = false;
}
@@ -754,22 +894,112 @@ async function handleCreateAddModuleProposal() {
});
if (result.success) {
console.log('[ModulesView] Предложение создано:', result);
alert('✅ Предложение для добавления модуля создано!');
console.log('[ModulesView] Данные транзакции получены:', result);
// Очищаем форму
newModule.value = {
moduleId: '',
moduleAddress: '',
description: '',
duration: 86400,
chainId: 11155111
};
// Перезагружаем модули
await loadModules();
// Отправляем транзакцию через MetaMask
try {
// Проверяем валидность адреса
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
}
// Проверяем, что адрес в правильном формате (checksum)
const isValidAddress = /^0x[a-fA-F0-9]{40}$/.test(result.data.to);
if (!isValidAddress) {
throw new Error(`Адрес не в правильном формате: ${result.data.to}`);
}
// Проверяем, что есть подключенный аккаунт
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (!accounts || accounts.length === 0) {
console.log('[ModulesView] Запрашиваем разрешение на подключение к MetaMask');
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
}
if (!accounts || accounts.length === 0) {
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
}
console.log('[ModulesView] Подключенный аккаунт:', accounts[0]);
// Проверяем подключение к правильной сети
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
const expectedChainId = '0x' + newModule.value.chainId.toString(16);
if (chainId !== expectedChainId) {
console.log(`[ModulesView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
try {
// Пытаемся переключиться на Sepolia
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: expectedChainId }],
});
console.log('[ModulesView] Успешно переключились на Sepolia');
} catch (switchError) {
// Если сеть не добавлена, добавляем её
if (switchError.code === 4902) {
console.log('[ModulesView] Добавляем Sepolia сеть');
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: expectedChainId,
chainName: 'Sepolia',
nativeCurrency: {
name: 'SepoliaETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
blockExplorerUrls: ['https://sepolia.etherscan.io']
}]
});
} else {
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
}
}
}
console.log('[ModulesView] Отправляем транзакцию:', {
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
});
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: result.data.to,
data: result.data.data,
value: result.data.value,
gas: result.data.gasLimit
}]
});
console.log('[ModulesView] Транзакция отправлена:', txHash);
alert(`✅ Транзакция отправлена! Hash: ${txHash}`);
// Очищаем форму
newModule.value = {
moduleId: '',
moduleAddress: '',
description: '',
duration: 86400,
chainId: 11155111
};
// Перезагружаем модули
await loadModules();
} catch (txError) {
console.error('[ModulesView] Ошибка отправки транзакции:', txError);
alert('❌ Ошибка отправки транзакции: ' + txError.message);
}
} else {
alert('❌ Ошибка создания предложения: ' + result.error);
alert('❌ Ошибка получения данных транзакции: ' + result.error);
}
} catch (error) {
@@ -854,7 +1084,8 @@ async function verifyModule(module, addressInfo) {
dleAddress: dleAddress,
moduleId: module.moduleId,
moduleAddress: addressInfo.address,
moduleName: module.moduleName
moduleName: module.moduleName,
chainId: addressInfo.chainId
});
if (response.data.success) {
@@ -898,16 +1129,12 @@ function getVerificationButtonTitle(verificationStatus) {
// Утилиты
function getEtherscanUrl(address, networkIndex, chainId) {
// Если есть chainId, используем его для определения правильного URL
if (chainId) {
const networkUrls = {
11155111: `https://sepolia.etherscan.io/address/${address}`, // Sepolia
17000: `https://holesky.etherscan.io/address/${address}`, // Holesky
421614: `https://sepolia.arbiscan.io/address/${address}`, // Arbitrum Sepolia
84532: `https://sepolia.basescan.org/address/${address}` // Base Sepolia
};
return networkUrls[chainId] || `https://etherscan.io/address/${address}`;
// Если есть chainId, ищем информацию о сети в supportedNetworks
if (chainId && supportedNetworks.value.length > 0) {
const network = supportedNetworks.value.find(n => n.chainId === chainId);
if (network && network.etherscanUrl) {
return `${network.etherscanUrl}/address/${address}`;
}
}
// Fallback на старую логику по networkIndex (для обратной совместимости)
@@ -1204,6 +1431,102 @@ onMounted(() => {
border: 1px solid #e9ecef;
}
/* Статус деплоя */
.deployment-status {
margin: 20px 0;
}
.status-loading {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.status-loading i {
color: #007bff;
font-size: 1.2rem;
}
.status-loading span {
color: #6c757d;
font-weight: 500;
}
.status-message {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 20px;
border-radius: 12px;
border: 2px solid;
}
.status-message.completed {
background-color: #e8f5e8;
border-color: #28a745;
}
.status-message.in_progress {
background-color: #e3f2fd;
border-color: #007bff;
}
.status-message.failed {
background-color: #ffebee;
border-color: #dc3545;
}
.status-message.not_started {
background-color: #fff3cd;
border-color: #ffc107;
}
.status-message.unknown {
background-color: #f8f9fa;
border-color: #6c757d;
}
.status-icon {
font-size: 2rem;
margin-top: 5px;
}
.status-message.completed .status-icon {
color: #28a745;
}
.status-message.in_progress .status-icon {
color: #007bff;
}
.status-message.failed .status-icon {
color: #dc3545;
}
.status-message.not_started .status-icon {
color: #ffc107;
}
.status-message.unknown .status-icon {
color: #6c757d;
}
.status-content h4 {
margin: 0 0 10px 0;
font-size: 1.1rem;
font-weight: 600;
}
.status-content p {
margin: 0;
color: #6c757d;
line-height: 1.5;
}
.list-header {
display: flex;
justify-content: space-between;

View File

@@ -0,0 +1,585 @@
<!--
Copyright (c) 2024-2025 Тарабанов Александр Викторович
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/HB3-ACCELERATOR
-->
<template>
<BaseLayout
:is-authenticated="isAuthenticated"
:identities="identities"
:token-balances="tokenBalances"
:is-loading-tokens="isLoadingTokens"
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="reader-module-deploy">
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Деплой DLEReader</h1>
<p>API для чтения данных DLE - получение информации о контракте и предложениях</p>
<p v-if="dleAddress" class="dle-address">
<strong>DLE:</strong> {{ dleAddress }}
</p>
</div>
<button class="close-btn" @click="router.push('/management/modules')">×</button>
</div>
<!-- Информация о модуле -->
<div class="module-info">
<div class="info-card">
<h3>📊 DLEReader</h3>
<div class="info-grid">
<div class="info-item">
<strong>Назначение:</strong> Чтение данных DLE контракта
</div>
<div class="info-item">
<strong>Функции:</strong> API для предложений, голосования, статистики
</div>
<div class="info-item">
<strong>Безопасность:</strong> Только чтение, не изменяет состояние
</div>
</div>
</div>
</div>
<!-- Форма деплоя модуля во всех сетях -->
<div class="deploy-form">
<div class="form-header">
<h3>🌐 Деплой DLEReader во всех сетях</h3>
<p>Деплой API модуля для чтения данных во всех 4 сетях одновременно</p>
</div>
<div class="form-content">
<!-- Информация о сетях -->
<div class="networks-info">
<h4>📡 Сети для деплоя:</h4>
<div class="networks-list">
<div class="network-item">
<span class="network-name">Sepolia</span>
<span class="network-chain-id">Chain ID: 11155111</span>
</div>
<div class="network-item">
<span class="network-name">Holesky</span>
<span class="network-chain-id">Chain ID: 17000</span>
</div>
<div class="network-item">
<span class="network-name">Arbitrum Sepolia</span>
<span class="network-chain-id">Chain ID: 421614</span>
</div>
<div class="network-item">
<span class="network-name">Base Sepolia</span>
<span class="network-chain-id">Chain ID: 84532</span>
</div>
</div>
</div>
<!-- Настройки модуля -->
<div class="module-settings">
<h4> Настройки DLEReader:</h4>
<div class="settings-form">
<div class="form-group">
<label for="chainId">ID сети:</label>
<select
id="chainId"
v-model="moduleSettings.chainId"
class="form-control"
required
>
<option value="11155111">Sepolia (11155111)</option>
<option value="17000">Holesky (17000)</option>
<option value="421614">Arbitrum Sepolia (421614)</option>
<option value="84532">Base Sepolia (84532)</option>
</select>
<small class="form-help">ID сети для деплоя модуля</small>
</div>
<div class="simple-info">
<h5>📋 Информация о DLEReader:</h5>
<div class="info-text">
<p><strong>DLEReader</strong> - это простой read-only модуль, который:</p>
<ul>
<li> Только читает данные из DLE контракта</li>
<li> Не изменяет состояние блокчейна</li>
<li> Предоставляет API для получения информации</li>
<li> Безопасен для обновления</li>
</ul>
<p><strong>Конструктор принимает только один параметр:</strong> адрес DLE контракта</p>
</div>
</div>
</div>
</div>
<!-- Кнопка деплоя -->
<div class="deploy-actions">
<button
class="btn btn-primary btn-large deploy-module"
@click="deployDLEReader"
:disabled="isDeploying || !dleAddress"
>
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
{{ isDeploying ? 'Деплой модуля...' : 'Деплой DLEReader' }}
</button>
<div v-if="deploymentProgress" class="deployment-progress">
<div class="progress-info">
<span>{{ deploymentProgress.message }}</span>
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import BaseLayout from '../../../components/BaseLayout.vue';
// Props
const props = defineProps({
isAuthenticated: Boolean,
identities: Array,
tokenBalances: Object,
isLoadingTokens: Boolean
});
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const route = useRoute();
// Состояние
const isLoading = ref(false);
const dleAddress = ref(route.query.address || null);
const isDeploying = ref(false);
const deploymentProgress = ref(null);
// Настройки модуля
const moduleSettings = ref({
// Единственный параметр - ID сети
chainId: 11155111
});
// Функция деплоя DLEReader
async function deployDLEReader() {
try {
isDeploying.value = true;
deploymentProgress.value = {
message: 'Инициализация деплоя...',
percentage: 0
};
console.log('[DLEReaderDeployView] Начинаем деплой DLEReader для DLE:', dleAddress.value);
// Вызываем API для деплоя модуля во всех сетях
const response = await fetch('/api/dle-modules/deploy-reader', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dleAddress: dleAddress.value,
moduleType: 'reader',
settings: {
// Единственный параметр - ID сети
chainId: moduleSettings.value.chainId
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
console.log('[DLEReaderDeployView] Деплой успешно запущен:', result);
// Обновляем прогресс
deploymentProgress.value = {
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
percentage: 100
};
alert('✅ Деплой DLEReader запущен во всех сетях!');
// Перенаправляем обратно к модулям
setTimeout(() => {
router.push(`/management/modules?address=${dleAddress.value}`);
}, 2000);
} else {
throw new Error(result.error || 'Неизвестная ошибка');
}
} catch (error) {
console.error('[DLEReaderDeployView] Ошибка деплоя:', error);
alert('❌ Ошибка деплоя: ' + error.message);
deploymentProgress.value = {
message: 'Ошибка деплоя: ' + error.message,
percentage: 0
};
} finally {
isDeploying.value = false;
}
}
// Инициализация
onMounted(() => {
console.log('[DLEReaderDeployView] Страница загружена');
});
</script>
<style scoped>
.reader-module-deploy {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius-lg);
color: white;
}
.header-content h1 {
margin: 0 0 10px 0;
font-size: 2rem;
font-weight: 700;
}
.header-content p {
margin: 0 0 5px 0;
opacity: 0.9;
font-size: 1.1rem;
}
.dle-address {
font-family: 'Courier New', monospace;
background: rgba(255, 255, 255, 0.1);
padding: 8px 12px;
border-radius: var(--radius-sm);
margin-top: 10px;
}
.close-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 24px;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.3);
color: white;
}
/* Информация о модуле */
.module-info {
margin-bottom: 30px;
}
.info-card {
background: white;
border-radius: var(--radius-md);
padding: 20px;
border: 1px solid #e9ecef;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.info-card h3 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.info-item {
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border-left: 4px solid var(--color-primary);
}
.info-item strong {
color: var(--color-primary);
}
/* Форма деплоя */
.deploy-form {
background: #f8f9fa;
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-header h3 {
margin: 0 0 10px 0;
color: var(--color-primary);
}
.form-header p {
margin: 0 0 20px 0;
color: #666;
}
.networks-info,
.module-settings {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.settings-form {
margin-top: 15px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* Настройки отображения данных */
.data-display-settings {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.data-display-settings h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.data-display-settings .form-row {
margin-bottom: 15px;
}
.data-display-settings .form-group {
margin-bottom: 15px;
}
.data-display-settings .form-group:last-child {
margin-bottom: 0;
}
/* Простая информация */
.simple-info {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.simple-info h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.info-text {
color: #666;
line-height: 1.6;
}
.info-text p {
margin: 0 0 10px 0;
}
.info-text ul {
margin: 10px 0;
padding-left: 20px;
}
.info-text li {
margin: 5px 0;
color: #555;
}
.info-text strong {
color: var(--color-primary);
}
.deploy-actions {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-large {
padding: 16px 32px;
font-size: 18px;
}
.deployment-progress {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-percentage {
font-weight: 600;
color: var(--color-primary);
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
transition: width 0.3s ease;
}
/* Сети */
.networks-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 10px;
}
.network-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.network-name {
font-weight: 600;
color: var(--color-primary);
}
.network-chain-id {
font-size: 12px;
color: #666;
font-family: 'Courier New', monospace;
}
</style>

View File

@@ -49,11 +49,255 @@
</div>
</div>
<!-- Форма деплоя будет добавлена позже -->
<div class="deploy-form-placeholder">
<div class="placeholder-content">
<h3>🚧 Форма деплоя в разработке</h3>
<p>Здесь будет форма для деплоя TimelockModule</p>
<!-- Форма деплоя модуля во всех сетях -->
<div class="deploy-form">
<div class="form-header">
<h3>🌐 Деплой TimelockModule во всех сетях</h3>
<p>Деплой модуля временных задержек во всех 4 сетях одновременно</p>
</div>
<div class="form-content">
<!-- Информация о сетях -->
<div class="networks-info">
<h4>📡 Сети для деплоя:</h4>
<div class="networks-list">
<div class="network-item">
<span class="network-name">Sepolia</span>
<span class="network-chain-id">Chain ID: 11155111</span>
</div>
<div class="network-item">
<span class="network-name">Holesky</span>
<span class="network-chain-id">Chain ID: 17000</span>
</div>
<div class="network-item">
<span class="network-name">Arbitrum Sepolia</span>
<span class="network-chain-id">Chain ID: 421614</span>
</div>
<div class="network-item">
<span class="network-name">Base Sepolia</span>
<span class="network-chain-id">Chain ID: 84532</span>
</div>
</div>
</div>
<!-- Настройки модуля -->
<div class="module-settings">
<h4> Настройки TimelockModule:</h4>
<div class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="chainId">ID сети:</label>
<select
id="chainId"
v-model="moduleSettings.chainId"
class="form-control"
required
>
<option value="11155111">Sepolia (11155111)</option>
<option value="17000">Holesky (17000)</option>
<option value="421614">Arbitrum Sepolia (421614)</option>
<option value="84532">Base Sepolia (84532)</option>
</select>
<small class="form-help">ID сети для деплоя модуля</small>
</div>
<div class="form-group">
<label for="defaultDelay">Стандартная задержка (дни):</label>
<input
type="number"
id="defaultDelay"
v-model="moduleSettings.defaultDelay"
class="form-control"
min="1"
max="30"
placeholder="2"
>
<small class="form-help">Стандартная задержка для операций (1-30 дней)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
<input
type="number"
id="emergencyDelay"
v-model="moduleSettings.emergencyDelay"
class="form-control"
min="5"
max="1440"
placeholder="30"
>
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
</div>
<div class="form-group">
<label for="maxDelay">Максимальная задержка (дни):</label>
<input
type="number"
id="maxDelay"
v-model="moduleSettings.maxDelay"
class="form-control"
min="1"
max="365"
placeholder="30"
>
<small class="form-help">Максимальная задержка для операций (1-365 дней)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="minDelay">Минимальная задержка (часы):</label>
<input
type="number"
id="minDelay"
v-model="moduleSettings.minDelay"
class="form-control"
min="1"
max="720"
placeholder="24"
>
<small class="form-help">Минимальная задержка для операций (1-720 часов)</small>
</div>
<div class="form-group">
<label for="maxOperations">Максимум операций в очереди:</label>
<input
type="number"
id="maxOperations"
v-model="moduleSettings.maxOperations"
class="form-control"
min="10"
max="1000"
placeholder="100"
>
<small class="form-help">Максимальное количество операций в очереди (10-1000)</small>
</div>
</div>
<!-- Дополнительные настройки таймлока -->
<div class="advanced-settings">
<h5>🔧 Дополнительные настройки таймлока:</h5>
<div class="form-group">
<label for="criticalOperations">Критические операции (JSON формат):</label>
<textarea
id="criticalOperations"
v-model="moduleSettings.criticalOperations"
class="form-control"
rows="3"
placeholder='["0x12345678", "0x87654321"]'
></textarea>
<small class="form-help">Селекторы функций, которые считаются критическими (JSON массив)</small>
</div>
<div class="form-group">
<label for="emergencyOperations">Экстренные операции (JSON формат):</label>
<textarea
id="emergencyOperations"
v-model="moduleSettings.emergencyOperations"
class="form-control"
rows="3"
placeholder='["0xabcdef12", "0x21fedcba"]'
></textarea>
<small class="form-help">Селекторы функций для экстренных операций (JSON массив)</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="operationDelays">Задержки для операций (JSON формат):</label>
<textarea
id="operationDelays"
v-model="moduleSettings.operationDelays"
class="form-control"
rows="4"
placeholder='{"0x12345678": 86400, "0x87654321": 172800}'
></textarea>
<small class="form-help">Кастомные задержки для конкретных операций (селектор => секунды)</small>
</div>
<div class="form-group">
<label for="autoExecuteEnabled">Автоисполнение включено:</label>
<select
id="autoExecuteEnabled"
v-model="moduleSettings.autoExecuteEnabled"
class="form-control"
>
<option value="true">Включено</option>
<option value="false">Отключено</option>
</select>
<small class="form-help">Автоматическое исполнение операций после истечения задержки</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="cancellationWindow">Окно отмены (часы):</label>
<input
type="number"
id="cancellationWindow"
v-model="moduleSettings.cancellationWindow"
class="form-control"
min="1"
max="168"
placeholder="24"
>
<small class="form-help">Время, в течение которого можно отменить операцию (1-168 часов)</small>
</div>
<div class="form-group">
<label for="executionWindow">Окно исполнения (часы):</label>
<input
type="number"
id="executionWindow"
v-model="moduleSettings.executionWindow"
class="form-control"
min="1"
max="168"
placeholder="48"
>
<small class="form-help">Время, в течение которого можно исполнить операцию (1-168 часов)</small>
</div>
</div>
<div class="form-group">
<label for="timelockDescription">Описание таймлока:</label>
<textarea
id="timelockDescription"
v-model="moduleSettings.timelockDescription"
class="form-control"
rows="2"
placeholder="Описание таймлока DLE для безопасности операций..."
></textarea>
<small class="form-help">Описание таймлока для документации</small>
</div>
</div>
</div>
</div>
<!-- Кнопка деплоя -->
<div class="deploy-actions">
<button
class="btn btn-primary btn-large deploy-module"
@click="deployTimelockModule"
:disabled="isDeploying || !dleAddress"
>
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TimelockModule' }}
</button>
<div v-if="deploymentProgress" class="deployment-progress">
<div class="progress-info">
<span>{{ deploymentProgress.message }}</span>
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
@@ -83,6 +327,108 @@ const route = useRoute();
// Состояние
const isLoading = ref(false);
const dleAddress = ref(route.query.address || null);
const isDeploying = ref(false);
const deploymentProgress = ref(null);
// Настройки модуля
const moduleSettings = ref({
// Основные параметры
chainId: 11155111,
defaultDelay: 2, // days
emergencyDelay: 30, // minutes
maxDelay: 30, // days
minDelay: 24, // hours
// Дополнительные настройки
maxOperations: 100,
criticalOperations: '',
emergencyOperations: '',
operationDelays: '',
autoExecuteEnabled: 'true',
cancellationWindow: 24, // hours
executionWindow: 48, // hours
timelockDescription: ''
});
// Функция деплоя TimelockModule
async function deployTimelockModule() {
try {
isDeploying.value = true;
deploymentProgress.value = {
message: 'Инициализация деплоя...',
percentage: 0
};
console.log('[TimelockModuleDeployView] Начинаем деплой TimelockModule для DLE:', dleAddress.value);
// Вызываем API для деплоя модуля во всех сетях
const response = await fetch('/api/dle-modules/deploy-timelock', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dleAddress: dleAddress.value,
moduleType: 'timelock',
settings: {
// Основные параметры
chainId: moduleSettings.value.chainId,
defaultDelay: moduleSettings.value.defaultDelay * 24 * 60 * 60, // конвертируем дни в секунды
emergencyDelay: moduleSettings.value.emergencyDelay * 60, // конвертируем минуты в секунды
maxDelay: moduleSettings.value.maxDelay * 24 * 60 * 60, // конвертируем дни в секунды
minDelay: moduleSettings.value.minDelay * 60 * 60, // конвертируем часы в секунды
// Дополнительные настройки
maxOperations: parseInt(moduleSettings.value.maxOperations),
criticalOperations: moduleSettings.value.criticalOperations ? JSON.parse(moduleSettings.value.criticalOperations) : [],
emergencyOperations: moduleSettings.value.emergencyOperations ? JSON.parse(moduleSettings.value.emergencyOperations) : [],
operationDelays: moduleSettings.value.operationDelays ? JSON.parse(moduleSettings.value.operationDelays) : {},
autoExecuteEnabled: moduleSettings.value.autoExecuteEnabled === 'true',
cancellationWindow: moduleSettings.value.cancellationWindow * 60 * 60, // конвертируем часы в секунды
executionWindow: moduleSettings.value.executionWindow * 60 * 60, // конвертируем часы в секунды
timelockDescription: moduleSettings.value.timelockDescription
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
console.log('[TimelockModuleDeployView] Деплой успешно запущен:', result);
// Обновляем прогресс
deploymentProgress.value = {
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
percentage: 100
};
alert('✅ Деплой TimelockModule запущен во всех сетях!');
// Перенаправляем обратно к модулям
setTimeout(() => {
router.push(`/management/modules?address=${dleAddress.value}`);
}, 2000);
} else {
throw new Error(result.error || 'Неизвестная ошибка');
}
} catch (error) {
console.error('[TimelockModuleDeployView] Ошибка деплоя:', error);
alert('❌ Ошибка деплоя: ' + error.message);
deploymentProgress.value = {
message: 'Ошибка деплоя: ' + error.message,
percentage: 0
};
} finally {
isDeploying.value = false;
}
}
// Инициализация
onMounted(() => {
@@ -150,6 +496,182 @@ onMounted(() => {
color: #333;
}
/* Форма деплоя */
.deploy-form {
background: #f8f9fa;
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-header h3 {
margin: 0 0 10px 0;
color: var(--color-primary);
}
.form-header p {
margin: 0 0 20px 0;
color: #666;
}
.networks-info,
.module-settings {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.settings-form {
margin-top: 15px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* Дополнительные настройки */
.advanced-settings {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.advanced-settings h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.advanced-settings .form-row {
margin-bottom: 15px;
}
.advanced-settings .form-group {
margin-bottom: 15px;
}
.advanced-settings .form-group:last-child {
margin-bottom: 0;
}
.deploy-actions {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-large {
padding: 16px 32px;
font-size: 18px;
}
.deployment-progress {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-percentage {
font-weight: 600;
color: var(--color-primary);
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
transition: width 0.3s ease;
}
/* Информация о модуле */
.module-info {
margin-bottom: 30px;

View File

@@ -49,11 +49,263 @@
</div>
</div>
<!-- Форма деплоя будет добавлена позже -->
<div class="deploy-form-placeholder">
<div class="placeholder-content">
<h3>🚧 Форма деплоя в разработке</h3>
<p>Здесь будет форма для деплоя TreasuryModule</p>
<!-- Форма деплоя модуля во всех сетях -->
<div class="deploy-form">
<div class="form-header">
<h3>🌐 Деплой TreasuryModule во всех сетях</h3>
<p>Деплой модуля казначейства во всех 4 сетях одновременно</p>
</div>
<div class="form-content">
<!-- Информация о сетях -->
<div class="networks-info">
<h4>📡 Сети для деплоя:</h4>
<div class="networks-list">
<div class="network-item">
<span class="network-name">Sepolia</span>
<span class="network-chain-id">Chain ID: 11155111</span>
</div>
<div class="network-item">
<span class="network-name">Holesky</span>
<span class="network-chain-id">Chain ID: 17000</span>
</div>
<div class="network-item">
<span class="network-name">Arbitrum Sepolia</span>
<span class="network-chain-id">Chain ID: 421614</span>
</div>
<div class="network-item">
<span class="network-name">Base Sepolia</span>
<span class="network-chain-id">Chain ID: 84532</span>
</div>
</div>
</div>
<!-- Настройки модуля -->
<div class="module-settings">
<h4> Настройки TreasuryModule:</h4>
<div class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="emergencyAdmin">Адрес экстренного администратора:</label>
<input
type="text"
id="emergencyAdmin"
v-model="moduleSettings.emergencyAdmin"
class="form-control"
placeholder="0x..."
required
>
<small class="form-help">Адрес экстренного администратора для управления модулем</small>
</div>
<div class="form-group">
<label for="chainId">ID сети:</label>
<select
id="chainId"
v-model="moduleSettings.chainId"
class="form-control"
required
>
<option value="11155111">Sepolia (11155111)</option>
<option value="17000">Holesky (17000)</option>
<option value="421614">Arbitrum Sepolia (421614)</option>
<option value="84532">Base Sepolia (84532)</option>
</select>
<small class="form-help">ID сети для деплоя модуля</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="defaultDelay">Стандартная задержка (часы):</label>
<input
type="number"
id="defaultDelay"
v-model="moduleSettings.defaultDelay"
class="form-control"
min="1"
max="720"
placeholder="24"
>
<small class="form-help">Стандартная задержка для операций (1-720 часов)</small>
</div>
<div class="form-group">
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
<input
type="number"
id="emergencyDelay"
v-model="moduleSettings.emergencyDelay"
class="form-control"
min="5"
max="1440"
placeholder="30"
>
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
</div>
</div>
<div class="form-group">
<label for="supportedTokens">Поддерживаемые токены (адреса через запятую):</label>
<textarea
id="supportedTokens"
v-model="moduleSettings.supportedTokens"
class="form-control"
rows="3"
placeholder="0x1234..., 0x5678..., 0x9abc..."
></textarea>
<small class="form-help">Адреса ERC20 токенов, которые будет поддерживать казначейство (через запятую)</small>
</div>
<div class="form-group">
<label for="gasPaymentTokens">Токены для оплаты газа (адреса через запятую):</label>
<textarea
id="gasPaymentTokens"
v-model="moduleSettings.gasPaymentTokens"
class="form-control"
rows="2"
placeholder="0x1234..., 0x5678..."
></textarea>
<small class="form-help">Токены, которыми можно оплачивать газ (через запятую)</small>
</div>
<!-- Дополнительные настройки казны -->
<div class="advanced-settings">
<h5>🔧 Дополнительные настройки казны:</h5>
<div class="form-row">
<div class="form-group">
<label for="paymasterAddress">Адрес Paymaster:</label>
<input
type="text"
id="paymasterAddress"
v-model="moduleSettings.paymasterAddress"
class="form-control"
placeholder="0x..."
>
<small class="form-help">Адрес Paymaster для ERC-4337 (оплата газа любым токеном)</small>
</div>
<div class="form-group">
<label for="maxBatchTransfers">Максимум batch переводов:</label>
<input
type="number"
id="maxBatchTransfers"
v-model="moduleSettings.maxBatchTransfers"
class="form-control"
min="1"
max="100"
placeholder="50"
>
<small class="form-help">Максимальное количество переводов в batch операции (1-100)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="gasTokenRates">Курсы токенов для газа (JSON формат):</label>
<textarea
id="gasTokenRates"
v-model="moduleSettings.gasTokenRates"
class="form-control"
rows="3"
placeholder='{"0x1234...": "1000000000000000000", "0x5678...": "2000000000000000000"}'
></textarea>
<small class="form-help">Курсы обмена токенов на нативную монету (JSON формат)</small>
</div>
<div class="form-group">
<label for="emergencyThreshold">Порог экстренных операций (ETH):</label>
<input
type="number"
id="emergencyThreshold"
v-model="moduleSettings.emergencyThreshold"
class="form-control"
min="0"
step="0.001"
placeholder="1.0"
>
<small class="form-help">Порог для экстренных операций в ETH</small>
</div>
</div>
<div class="form-group">
<label for="initialTokens">Начальные токены для добавления (JSON формат):</label>
<textarea
id="initialTokens"
v-model="moduleSettings.initialTokens"
class="form-control"
rows="4"
placeholder='[{"address": "0x1234...", "symbol": "USDC", "decimals": 6}, {"address": "0x5678...", "symbol": "USDT", "decimals": 6}]'
></textarea>
<small class="form-help">Токены для автоматического добавления при деплое (JSON массив)</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="autoRefreshBalances">Автообновление балансов:</label>
<select
id="autoRefreshBalances"
v-model="moduleSettings.autoRefreshBalances"
class="form-control"
>
<option value="true">Включено</option>
<option value="false">Отключено</option>
</select>
<small class="form-help">Автоматическое обновление балансов токенов</small>
</div>
<div class="form-group">
<label for="batchTransferEnabled">Batch переводы включены:</label>
<select
id="batchTransferEnabled"
v-model="moduleSettings.batchTransferEnabled"
class="form-control"
>
<option value="true">Включено</option>
<option value="false">Отключено</option>
</select>
<small class="form-help">Разрешить batch операции переводов</small>
</div>
</div>
<div class="form-group">
<label for="treasuryDescription">Описание казны:</label>
<textarea
id="treasuryDescription"
v-model="moduleSettings.treasuryDescription"
class="form-control"
rows="2"
placeholder="Описание казны DLE для управления финансами..."
></textarea>
<small class="form-help">Описание казны для документации</small>
</div>
</div>
</div>
</div>
<!-- Кнопка деплоя -->
<div class="deploy-actions">
<button
class="btn btn-primary btn-large deploy-module"
@click="deployTreasuryModule"
:disabled="isDeploying || !dleAddress"
>
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TreasuryModule' }}
</button>
<div v-if="deploymentProgress" class="deployment-progress">
<div class="progress-info">
<span>{{ deploymentProgress.message }}</span>
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
</div>
</div>
</div>
</div>
</div>
@@ -83,6 +335,114 @@ const route = useRoute();
// Состояние
const isLoading = ref(false);
const dleAddress = ref(route.query.address || null);
const isDeploying = ref(false);
const deploymentProgress = ref(null);
// Настройки модуля
const moduleSettings = ref({
// Основные параметры
emergencyAdmin: '',
chainId: 11155111,
defaultDelay: 24, // hours
emergencyDelay: 30, // minutes
// Токены
supportedTokens: '',
gasPaymentTokens: '',
initialTokens: '',
// Дополнительные настройки
paymasterAddress: '',
maxBatchTransfers: 50,
gasTokenRates: '',
emergencyThreshold: 1.0,
autoRefreshBalances: 'true',
batchTransferEnabled: 'true',
treasuryDescription: ''
});
// Функция деплоя TreasuryModule
async function deployTreasuryModule() {
try {
isDeploying.value = true;
deploymentProgress.value = {
message: 'Инициализация деплоя...',
percentage: 0
};
console.log('[TreasuryModuleDeployView] Начинаем деплой TreasuryModule для DLE:', dleAddress.value);
// Вызываем API для деплоя модуля во всех сетях
const response = await fetch('/api/dle-modules/deploy-treasury', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
dleAddress: dleAddress.value,
moduleType: 'treasury',
settings: {
// Основные параметры
emergencyAdmin: moduleSettings.value.emergencyAdmin,
chainId: moduleSettings.value.chainId,
defaultDelay: moduleSettings.value.defaultDelay,
emergencyDelay: moduleSettings.value.emergencyDelay,
// Токены
supportedTokens: moduleSettings.value.supportedTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
gasPaymentTokens: moduleSettings.value.gasPaymentTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
initialTokens: moduleSettings.value.initialTokens ? JSON.parse(moduleSettings.value.initialTokens) : [],
// Дополнительные настройки
paymasterAddress: moduleSettings.value.paymasterAddress,
maxBatchTransfers: parseInt(moduleSettings.value.maxBatchTransfers),
gasTokenRates: moduleSettings.value.gasTokenRates ? JSON.parse(moduleSettings.value.gasTokenRates) : {},
emergencyThreshold: parseFloat(moduleSettings.value.emergencyThreshold),
autoRefreshBalances: moduleSettings.value.autoRefreshBalances === 'true',
batchTransferEnabled: moduleSettings.value.batchTransferEnabled === 'true',
treasuryDescription: moduleSettings.value.treasuryDescription
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
console.log('[TreasuryModuleDeployView] Деплой успешно запущен:', result);
// Обновляем прогресс
deploymentProgress.value = {
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
percentage: 100
};
alert('✅ Деплой TreasuryModule запущен во всех сетях!');
// Перенаправляем обратно к модулям
setTimeout(() => {
router.push(`/management/modules?address=${dleAddress.value}`);
}, 2000);
} else {
throw new Error(result.error || 'Неизвестная ошибка');
}
} catch (error) {
console.error('[TreasuryModuleDeployView] Ошибка деплоя:', error);
alert('❌ Ошибка деплоя: ' + error.message);
deploymentProgress.value = {
message: 'Ошибка деплоя: ' + error.message,
percentage: 0
};
} finally {
isDeploying.value = false;
}
}
// Инициализация
onMounted(() => {
@@ -150,6 +510,240 @@ onMounted(() => {
color: #333;
}
/* Форма деплоя */
.deploy-form {
background: #f8f9fa;
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 30px;
border: 1px solid #e9ecef;
}
.form-header h3 {
margin: 0 0 10px 0;
color: var(--color-primary);
}
.form-header p {
margin: 0 0 20px 0;
color: #666;
}
.networks-info,
.module-settings {
margin-bottom: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.settings-form {
margin-top: 15px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 14px;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
.form-help {
display: block;
margin-top: 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
}
/* Дополнительные настройки */
.advanced-settings {
margin-top: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
}
.advanced-settings h5 {
margin: 0 0 15px 0;
color: var(--color-primary);
font-size: 1.1rem;
font-weight: 600;
}
.advanced-settings .form-row {
margin-bottom: 15px;
}
.advanced-settings .form-group {
margin-bottom: 15px;
}
.advanced-settings .form-group:last-child {
margin-bottom: 0;
}
.networks-info h4,
.deploy-parameters h4 {
margin: 0 0 15px 0;
color: var(--color-primary);
}
.networks-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
.network-item {
display: flex;
flex-direction: column;
padding: 10px;
background: #f8f9fa;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.network-name {
font-weight: 600;
color: var(--color-primary);
}
.network-chain-id {
font-size: 12px;
color: #666;
font-family: monospace;
}
.parameter-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.parameter-item:last-child {
border-bottom: none;
}
.parameter-item label {
font-weight: 500;
color: #333;
}
.parameter-value {
font-family: monospace;
color: var(--color-primary);
background: #f8f9fa;
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 14px;
}
.deploy-actions {
text-align: center;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-large {
padding: 16px 32px;
font-size: 18px;
}
.deployment-progress {
margin-top: 20px;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #dee2e6;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-percentage {
font-weight: 600;
color: var(--color-primary);
}
.progress-bar {
width: 100%;
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
transition: width 0.3s ease;
}
/* Информация о модуле */
.module-info {
margin-bottom: 30px;

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* 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/HB3-ACCELERATOR
*/
// Основные модули DLE
export { default as TreasuryModuleDeployView } from './TreasuryModuleDeployView.vue';
export { default as TimelockModuleDeployView } from './TimelockModuleDeployView.vue';
export { default as DLEReaderDeployView } from './DLEReaderDeployView.vue';
// Дополнительные модули
export { default as CommunicationModuleDeployView } from './CommunicationModuleDeployView.vue';
export { default as ApplicationModuleDeployView } from './ApplicationModuleDeployView.vue';
export { default as MintModuleDeploy } from './MintModuleDeploy.vue';
export { default as BurnModuleDeploy } from './BurnModuleDeploy.vue';
export { default as OracleModuleDeploy } from './OracleModuleDeploy.vue';
export { default as InheritanceModuleDeploy } from './InheritanceModuleDeploy.vue';
// Кастомный модуль
export { default as ModuleDeployFormView } from './ModuleDeployFormView.vue';