feat: Добавлены формы деплоя модулей DLE с полными настройками
- Создана форма деплоя TreasuryModule с детальными настройками казны - Создана форма деплоя TimelockModule с настройками временных задержек - Создана форма деплоя DLEReader с простой конфигурацией - Добавлены маршруты и индексы для всех модулей - Исправлены пути импорта BaseLayout - Добавлены авторские права во все файлы - Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
@@ -12,10 +12,11 @@
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
// Создаем экземпляр axios с базовым URL
|
||||
// Создаем экземпляр axios с базовым URL и таймаутами
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
withCredentials: true,
|
||||
timeout: 10 * 60 * 1000, // 10 минут таймаут для деплоя
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -25,15 +26,36 @@ const api = axios.create({
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
config.withCredentials = true; // Важно для каждого запроса
|
||||
|
||||
// DEBUG: логируем все исходящие запросы
|
||||
console.log('🌐 [AXIOS] Отправляем запрос:', {
|
||||
method: config.method?.toUpperCase(),
|
||||
url: config.url,
|
||||
baseURL: config.baseURL,
|
||||
fullURL: config.baseURL + config.url,
|
||||
data: config.data ? '[ДАННЫЕ]' : 'нет данных'
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
(error) => {
|
||||
console.error('🌐 [AXIOS] Ошибка перед отправкой:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Добавляем перехватчик ответов для обработки ошибок
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
// DEBUG: логируем успешные ответы
|
||||
console.log('🌐 [AXIOS] Получен ответ:', {
|
||||
method: response.config.method?.toUpperCase(),
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
contentType: response.headers['content-type']
|
||||
});
|
||||
|
||||
// Проверяем, что ответ действительно JSON
|
||||
if (response.headers['content-type'] &&
|
||||
!response.headers['content-type'].includes('application/json')) {
|
||||
@@ -46,6 +68,16 @@ api.interceptors.response.use(
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// DEBUG: логируем ошибки
|
||||
console.error('🌐 [AXIOS] Ошибка ответа:', {
|
||||
method: error.config?.method?.toUpperCase(),
|
||||
url: error.config?.url,
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText
|
||||
});
|
||||
|
||||
// Если ошибка содержит HTML в response
|
||||
if (error.response && error.response.data &&
|
||||
typeof error.response.data === 'string' &&
|
||||
|
||||
601
frontend/src/components/deployment/DeploymentWizard.vue
Normal file
601
frontend/src/components/deployment/DeploymentWizard.vue
Normal file
@@ -0,0 +1,601 @@
|
||||
<!--
|
||||
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>
|
||||
<div class="deployment-wizard">
|
||||
<!-- Заголовок -->
|
||||
<div class="wizard-header">
|
||||
<h2 class="wizard-title">Мастер поэтапного деплоя DLE</h2>
|
||||
<p class="wizard-subtitle">
|
||||
Автоматический деплой DLE контракта и модулей с WebSocket обновлениями в реальном времени
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Прогресс-бар -->
|
||||
<div class="progress-section">
|
||||
<div class="progress-bar">
|
||||
<div
|
||||
class="progress-fill"
|
||||
:style="{ width: `${progressPercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
{{ currentStage }} ({{ progressPercentage }}%)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Статус деплоя -->
|
||||
<div class="status-section">
|
||||
<div class="status-card" :class="statusClass">
|
||||
<div class="status-icon">
|
||||
<i :class="statusIcon"></i>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<h3 class="status-title">{{ statusTitle }}</h3>
|
||||
<p class="status-message">{{ statusMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Логи операций -->
|
||||
<div class="logs-section">
|
||||
<div class="logs-header">
|
||||
<h3>Логи операций</h3>
|
||||
<button
|
||||
class="clear-logs-btn"
|
||||
@click="clearLogs"
|
||||
:disabled="isDeploying"
|
||||
>
|
||||
Очистить
|
||||
</button>
|
||||
</div>
|
||||
<div class="logs-container" ref="logsContainer">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
:class="['log-entry', `log-${log.type}`]"
|
||||
>
|
||||
<span class="log-time">{{ log.timestamp }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="no-logs">
|
||||
Логи операций будут отображаться здесь
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Сетевые статусы -->
|
||||
<div v-if="Object.keys(networksStatus).length > 0" class="networks-section">
|
||||
<h3>Статус по сетям</h3>
|
||||
<div class="networks-grid">
|
||||
<div
|
||||
v-for="(network, chainId) in networksStatus"
|
||||
:key="chainId"
|
||||
:class="['network-item', `network-${network.status}`]"
|
||||
>
|
||||
<div class="network-name">{{ getNetworkName(chainId) }}</div>
|
||||
<div class="network-status">{{ network.status }}</div>
|
||||
<div v-if="network.address" class="network-address">{{ network.address.substring(0, 10) }}...</div>
|
||||
<div v-if="network.message" class="network-message">{{ network.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки управления -->
|
||||
<div class="controls-section">
|
||||
<button
|
||||
class="stop-btn"
|
||||
@click="stopDeploymentTracking"
|
||||
v-if="isDeploying"
|
||||
>
|
||||
<i class="fas fa-stop"></i>
|
||||
Остановить отслеживание
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="reset-btn"
|
||||
@click="resetDeploymentState"
|
||||
v-if="deploymentStatus === 'completed' || deploymentStatus === 'failed'"
|
||||
>
|
||||
<i class="fas fa-redo"></i>
|
||||
Сбросить состояние
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Ошибка -->
|
||||
<div v-if="error" class="error-section">
|
||||
<div class="error-card">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<div>
|
||||
<h4>Произошла ошибка</h4>
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick, onMounted } from 'vue';
|
||||
import { useDeploymentWebSocket } from '@/composables/useDeploymentWebSocket';
|
||||
import api from '@/api/axios';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
dleAddress: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
privateKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
selectedNetworks: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
dleData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
etherscanApiKey: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
// Events
|
||||
const emit = defineEmits(['deployment-completed']);
|
||||
|
||||
// WebSocket композабл для деплоя
|
||||
const {
|
||||
deploymentStatus,
|
||||
currentStage,
|
||||
progress,
|
||||
isDeploying,
|
||||
logs,
|
||||
deploymentResult,
|
||||
networksStatus,
|
||||
error,
|
||||
startDeploymentTracking,
|
||||
stopDeploymentTracking,
|
||||
resetDeploymentState,
|
||||
addLog,
|
||||
clearLogs
|
||||
} = useDeploymentWebSocket();
|
||||
|
||||
// Ссылка на контейнер логов
|
||||
const logsContainer = ref(null);
|
||||
|
||||
// Вычисляемые свойства
|
||||
const progressPercentage = computed(() => {
|
||||
return Math.round((progress.value || 0));
|
||||
});
|
||||
|
||||
const statusClass = computed(() => {
|
||||
switch (deploymentStatus.value) {
|
||||
case 'completed': return 'status-success';
|
||||
case 'failed': return 'status-error';
|
||||
case 'in_progress': return 'status-running';
|
||||
default: return 'status-pending';
|
||||
}
|
||||
});
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
switch (deploymentStatus.value) {
|
||||
case 'completed': return 'fas fa-check-circle';
|
||||
case 'failed': return 'fas fa-times-circle';
|
||||
case 'in_progress': return 'fas fa-spinner fa-spin';
|
||||
default: return 'fas fa-clock';
|
||||
}
|
||||
});
|
||||
|
||||
const statusTitle = computed(() => {
|
||||
switch (deploymentStatus.value) {
|
||||
case 'not_started': return 'Готов к запуску';
|
||||
case 'in_progress': return 'Выполняется деплой';
|
||||
case 'completed': return 'Деплой завершен';
|
||||
case 'failed': return 'Ошибка деплоя';
|
||||
default: return 'Неизвестный статус';
|
||||
}
|
||||
});
|
||||
|
||||
const statusMessage = computed(() => {
|
||||
switch (deploymentStatus.value) {
|
||||
case 'not_started': return 'Готов к автоматическому развертыванию через WebSocket';
|
||||
case 'in_progress': return `Выполняется: ${currentStage.value || 'инициализация'}`;
|
||||
case 'completed': return 'Все этапы деплоя успешно завершены!';
|
||||
case 'failed': return 'Произошла ошибка. Проверьте логи для деталей.';
|
||||
default: return '';
|
||||
}
|
||||
});
|
||||
|
||||
// Функции
|
||||
const getNetworkName = (chainId) => {
|
||||
const networkNames = {
|
||||
'1': 'Ethereum',
|
||||
'11155111': 'Sepolia',
|
||||
'421614': 'Arbitrum Sepolia',
|
||||
'84532': 'Base Sepolia',
|
||||
'17000': 'Holesky'
|
||||
};
|
||||
return networkNames[chainId] || `Network ${chainId}`;
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (logsContainer.value) {
|
||||
logsContainer.value.scrollTop = logsContainer.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Главная функция запуска деплоя
|
||||
const startDeployment = async () => {
|
||||
try {
|
||||
addLog('🚀 Начинаем асинхронный деплой с WebSocket отслеживанием', 'info');
|
||||
|
||||
// Подготовка данных для деплоя
|
||||
const deployData = {
|
||||
name: props.dleData.name,
|
||||
symbol: props.dleData.tokenSymbol,
|
||||
location: props.dleData.addressData?.fullAddress || 'Не указан',
|
||||
coordinates: props.dleData.coordinates || '0,0',
|
||||
jurisdiction: parseInt(props.dleData.jurisdiction) || 0,
|
||||
oktmo: props.dleData.selectedOktmo || '',
|
||||
okvedCodes: props.dleData.selectedOkved || [],
|
||||
kpp: props.dleData.kppCode || '',
|
||||
quorumPercentage: props.dleData.governanceQuorum || 51,
|
||||
initialPartners: props.dleData.partners.map(p => p.address).filter(addr => addr),
|
||||
initialAmounts: props.dleData.partners.map(p => p.amount).filter(amount => amount > 0),
|
||||
supportedChainIds: props.selectedNetworks.filter(id => id !== null && id !== undefined),
|
||||
currentChainId: props.selectedNetworks[0] || 1,
|
||||
privateKey: props.privateKey,
|
||||
etherscanApiKey: props.etherscanApiKey || '',
|
||||
autoVerifyAfterDeploy: false
|
||||
};
|
||||
|
||||
addLog('📤 Отправляем запрос на асинхронный деплой...', 'info');
|
||||
|
||||
// Отправляем запрос на асинхронный деплой (без таймаута!)
|
||||
const response = await api.post('/dle-v2', deployData);
|
||||
|
||||
if (response.data.success && response.data.deploymentId) {
|
||||
addLog(`✅ Деплой запущен! ID: ${response.data.deploymentId}`, 'success');
|
||||
|
||||
// Начинаем отслеживание через WebSocket
|
||||
startDeploymentTracking(response.data.deploymentId);
|
||||
|
||||
} else {
|
||||
throw new Error('Не удалось запустить деплой: ' + (response.data.message || 'неизвестная ошибка'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
addLog(`❌ Ошибка запуска деплоя: ${error.message}`, 'error');
|
||||
console.error('Deployment start failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Автозапуск деплоя при появлении компонента
|
||||
onMounted(() => {
|
||||
if (deploymentStatus.value === 'not_started') {
|
||||
addLog('🚀 Автоматически запускаем деплой...', 'info');
|
||||
startDeployment();
|
||||
}
|
||||
});
|
||||
|
||||
// Следим за новыми логами и скроллим вниз
|
||||
watch(logs, () => {
|
||||
scrollToBottom();
|
||||
}, { deep: true });
|
||||
|
||||
// Следим за завершением деплоя
|
||||
watch(deploymentStatus, (newStatus) => {
|
||||
if (newStatus === 'completed' && deploymentResult.value) {
|
||||
addLog('🎉 Деплой успешно завершен! Перенаправляем на страницу управления...', 'success');
|
||||
setTimeout(() => {
|
||||
emit('deployment-completed', deploymentResult.value);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deployment-wizard {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wizard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.wizard-title {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 10px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.wizard-subtitle {
|
||||
color: #7f8c8d;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #ecf0f1;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3498db, #2ecc71);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #bdc3c7;
|
||||
}
|
||||
|
||||
.status-card.status-pending {
|
||||
border-color: #f39c12;
|
||||
background-color: #fef9e7;
|
||||
}
|
||||
|
||||
.status-card.status-running {
|
||||
border-color: #3498db;
|
||||
background-color: #ebf3fd;
|
||||
}
|
||||
|
||||
.status-card.status-success {
|
||||
border-color: #2ecc71;
|
||||
background-color: #eafaf1;
|
||||
}
|
||||
|
||||
.status-card.status-error {
|
||||
border-color: #e74c3c;
|
||||
background-color: #fdf2f2;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 2em;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.status-content h3 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.status-content p {
|
||||
margin: 0;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.logs-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.clear-logs-btn {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-logs-btn:hover:not(:disabled) {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #95a5a6;
|
||||
font-size: 0.9em;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-info { color: #3498db; }
|
||||
.log-success { color: #2ecc71; }
|
||||
.log-error { color: #e74c3c; }
|
||||
.log-warning { color: #f39c12; }
|
||||
|
||||
.no-logs {
|
||||
text-align: center;
|
||||
color: #95a5a6;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.networks-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.networks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #bdc3c7;
|
||||
}
|
||||
|
||||
.network-item.network-pending {
|
||||
border-color: #f39c12;
|
||||
background-color: #fef9e7;
|
||||
}
|
||||
|
||||
.network-item.network-in_progress {
|
||||
border-color: #3498db;
|
||||
background-color: #ebf3fd;
|
||||
}
|
||||
|
||||
.network-item.network-completed {
|
||||
border-color: #2ecc71;
|
||||
background-color: #eafaf1;
|
||||
}
|
||||
|
||||
.network-item.network-failed {
|
||||
border-color: #e74c3c;
|
||||
background-color: #fdf2f2;
|
||||
}
|
||||
|
||||
.network-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.network-status {
|
||||
font-size: 0.9em;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.network-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.8em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.network-message {
|
||||
font-size: 0.8em;
|
||||
color: #7f8c8d;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.start-btn, .stop-btn, .reset-btn {
|
||||
padding: 15px 30px;
|
||||
font-size: 1.1em;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.start-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #27ae60, #219a52);
|
||||
}
|
||||
|
||||
.start-btn:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.stop-btn {
|
||||
background: linear-gradient(135deg, #e74c3c, #c0392b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stop-btn:hover {
|
||||
background: linear-gradient(135deg, #c0392b, #a93226);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: linear-gradient(135deg, #2980b9, #21618c);
|
||||
}
|
||||
|
||||
.error-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.error-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: #fdf2f2;
|
||||
border: 2px solid #e74c3c;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.error-card i {
|
||||
color: #e74c3c;
|
||||
font-size: 1.5em;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.error-card h4 {
|
||||
margin: 0 0 5px 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.error-card p {
|
||||
margin: 0;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -60,7 +60,6 @@ export default function useBlockchainNetworks() {
|
||||
{ value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 },
|
||||
{ value: 'arbitrum-sepolia', label: 'Arbitrum Sepolia', chainId: 421614 },
|
||||
{ value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 },
|
||||
{ value: 'avalanche-fuji', label: 'Avalanche Fuji', chainId: 43113 },
|
||||
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 },
|
||||
{ value: 'base-sepolia', label: 'Base Sepolia Testnet', chainId: 84532 }
|
||||
]
|
||||
|
||||
210
frontend/src/composables/useDeploymentWebSocket.js
Normal file
210
frontend/src/composables/useDeploymentWebSocket.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { ref, reactive, onUnmounted } from 'vue';
|
||||
import wsClient from '../utils/websocket';
|
||||
|
||||
export function useDeploymentWebSocket() {
|
||||
// Состояние деплоя
|
||||
const deploymentStatus = ref('not_started'); // not_started, in_progress, completed, failed
|
||||
const currentStage = ref('');
|
||||
const currentNetwork = ref('');
|
||||
const progress = ref(0);
|
||||
const isDeploying = ref(false);
|
||||
const deploymentId = ref(null);
|
||||
const logs = ref([]);
|
||||
const error = ref(null);
|
||||
|
||||
// Детальная информация по сетям
|
||||
const networksStatus = reactive({});
|
||||
|
||||
// Результат деплоя
|
||||
const deploymentResult = ref(null);
|
||||
|
||||
// Добавить лог
|
||||
const addLog = (message, type = 'info') => {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logs.value.push({
|
||||
timestamp,
|
||||
message,
|
||||
type
|
||||
});
|
||||
};
|
||||
|
||||
// Очистить логи
|
||||
const clearLogs = () => {
|
||||
logs.value = [];
|
||||
};
|
||||
|
||||
// Обработчик WebSocket сообщений
|
||||
const handleDeploymentUpdate = (data) => {
|
||||
if (data.deploymentId !== deploymentId.value) return;
|
||||
|
||||
console.log('🔄 [DeploymentWebSocket] Получено обновление:', data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'deployment_started':
|
||||
deploymentStatus.value = 'in_progress';
|
||||
isDeploying.value = true;
|
||||
currentStage.value = data.stage || '';
|
||||
addLog(`🚀 ${data.message}`, 'info');
|
||||
break;
|
||||
|
||||
case 'deployment_progress':
|
||||
currentStage.value = data.stage || '';
|
||||
currentNetwork.value = data.network || '';
|
||||
progress.value = data.progress || 0;
|
||||
if (data.message) {
|
||||
addLog(`📊 ${data.message}`, 'info');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_stage_completed':
|
||||
if (data.message) {
|
||||
addLog(`✅ ${data.message}`, 'success');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_network_update':
|
||||
if (data.network) {
|
||||
networksStatus[data.network] = {
|
||||
status: data.status,
|
||||
address: data.address,
|
||||
message: data.message
|
||||
};
|
||||
}
|
||||
if (data.message) {
|
||||
addLog(`🌐 [${data.network}] ${data.message}`, 'info');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_error':
|
||||
error.value = data.error;
|
||||
if (data.message) {
|
||||
addLog(`❌ ${data.message}`, 'error');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deployment_completed':
|
||||
deploymentStatus.value = 'completed';
|
||||
isDeploying.value = false;
|
||||
deploymentResult.value = data.result;
|
||||
progress.value = 100;
|
||||
addLog(`🎉 ${data.message}`, 'success');
|
||||
break;
|
||||
|
||||
case 'deployment_failed':
|
||||
deploymentStatus.value = 'failed';
|
||||
isDeploying.value = false;
|
||||
error.value = data.error;
|
||||
addLog(`💥 ${data.message}`, 'error');
|
||||
break;
|
||||
|
||||
case 'deployment_log':
|
||||
if (data.log) {
|
||||
addLog(data.log.message, data.log.type || 'info');
|
||||
}
|
||||
break;
|
||||
|
||||
case undefined:
|
||||
// Обработка событий без типа (прямые обновления)
|
||||
if (data.stage) currentStage.value = data.stage;
|
||||
if (data.progress !== undefined) progress.value = data.progress;
|
||||
if (data.status) deploymentStatus.value = data.status;
|
||||
if (data.result) deploymentResult.value = data.result;
|
||||
if (data.error) error.value = data.error;
|
||||
if (data.status === 'completed') {
|
||||
isDeploying.value = false;
|
||||
addLog('🎉 Деплой успешно завершен!', 'success');
|
||||
} else if (data.status === 'failed') {
|
||||
isDeploying.value = false;
|
||||
addLog('💥 Деплой завершился с ошибкой!', 'error');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('🤷♂️ [DeploymentWebSocket] Неизвестный тип события:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
// Начать отслеживание деплоя
|
||||
const startDeploymentTracking = (id) => {
|
||||
console.log('🎯 [DeploymentWebSocket] Начинаем отслеживание деплоя:', id);
|
||||
|
||||
deploymentId.value = id;
|
||||
deploymentStatus.value = 'in_progress';
|
||||
isDeploying.value = true;
|
||||
clearLogs();
|
||||
|
||||
// Подключаемся к WebSocket обновлениям
|
||||
wsClient.connect();
|
||||
if (wsClient && typeof wsClient.subscribe === 'function') {
|
||||
wsClient.subscribe('deployment_update', handleDeploymentUpdate);
|
||||
} else {
|
||||
console.warn('[DeploymentWebSocket] wsClient.subscribe недоступен');
|
||||
}
|
||||
|
||||
addLog('🔌 Подключено к WebSocket для получения обновлений деплоя', 'info');
|
||||
};
|
||||
|
||||
// Остановить отслеживание
|
||||
const stopDeploymentTracking = () => {
|
||||
console.log('🛑 [DeploymentWebSocket] Останавливаем отслеживание');
|
||||
|
||||
if (wsClient && typeof wsClient.unsubscribe === 'function') {
|
||||
wsClient.unsubscribe('deployment_update', handleDeploymentUpdate);
|
||||
} else {
|
||||
console.warn('[DeploymentWebSocket] wsClient.unsubscribe недоступен');
|
||||
}
|
||||
isDeploying.value = false;
|
||||
};
|
||||
|
||||
// Очистить состояние
|
||||
const resetDeploymentState = () => {
|
||||
deploymentStatus.value = 'not_started';
|
||||
currentStage.value = '';
|
||||
currentNetwork.value = '';
|
||||
progress.value = 0;
|
||||
isDeploying.value = false;
|
||||
deploymentId.value = null;
|
||||
error.value = null;
|
||||
deploymentResult.value = null;
|
||||
clearLogs();
|
||||
Object.keys(networksStatus).forEach(key => delete networksStatus[key]);
|
||||
};
|
||||
|
||||
// Автоматическая отписка при размонтировании компонента
|
||||
onUnmounted(() => {
|
||||
stopDeploymentTracking();
|
||||
});
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
deploymentStatus,
|
||||
currentStage,
|
||||
currentNetwork,
|
||||
progress,
|
||||
isDeploying,
|
||||
deploymentId,
|
||||
logs,
|
||||
error,
|
||||
networksStatus,
|
||||
deploymentResult,
|
||||
|
||||
// Методы
|
||||
startDeploymentTracking,
|
||||
stopDeploymentTracking,
|
||||
resetDeploymentState,
|
||||
addLog,
|
||||
clearLogs
|
||||
};
|
||||
}
|
||||
@@ -242,6 +242,11 @@ const routes = [
|
||||
name: 'module-deploy-timelock',
|
||||
component: () => import('../views/smartcontracts/modules/TimelockModuleDeployView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/modules/deploy/reader',
|
||||
name: 'module-deploy-reader',
|
||||
component: () => import('../views/smartcontracts/modules/DLEReaderDeployView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/modules/deploy/communication',
|
||||
name: 'module-deploy-communication',
|
||||
|
||||
@@ -15,20 +15,6 @@ import axios from 'axios';
|
||||
|
||||
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
|
||||
|
||||
/**
|
||||
* Создает новое DLE v2
|
||||
* @param {Object} dleParams - Параметры DLE
|
||||
* @returns {Promise<Object>} - Результат создания
|
||||
*/
|
||||
export const createDLE = async (dleParams) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-v2', dleParams);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при создании DLE:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает список всех DLE v2
|
||||
@@ -59,34 +45,7 @@ export const getDLEInfo = async (dleAddress) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает параметры по умолчанию для создания DLE v2
|
||||
* @returns {Promise<Object>} - Параметры по умолчанию
|
||||
*/
|
||||
export const getDefaultParams = async () => {
|
||||
try {
|
||||
const response = await axios.get('/dle-v2/default-params');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении параметров по умолчанию:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Читает данные DLE из блокчейна
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @returns {Promise<Object>} - Данные из блокчейна
|
||||
*/
|
||||
export const readDLEFromBlockchain = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-core/read-dle-info', { dleAddress });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при чтении DLE из блокчейна:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает параметры управления DLE
|
||||
@@ -128,35 +87,12 @@ export const getSupportedChains = async (dleAddress) => {
|
||||
* @param {number} chainId - ID сети
|
||||
* @returns {Promise<Object>} - Статус поддержки
|
||||
*/
|
||||
export const isChainSupported = async (dleAddress, chainId) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-multichain/is-chain-supported', {
|
||||
dleAddress,
|
||||
chainId
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке поддержки сети:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает текущую сеть
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @returns {Promise<Object>} - Текущая сеть
|
||||
*/
|
||||
export const getCurrentChainId = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-current-chain-id', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении текущей сети:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Исполняет предложение по подписям
|
||||
@@ -164,18 +100,6 @@ export const getCurrentChainId = async (dleAddress) => {
|
||||
* @param {Object} executionData - Данные исполнения
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export const executeProposalBySignatures = async (dleAddress, executionData) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-multichain/execute-proposal-by-signatures', {
|
||||
dleAddress,
|
||||
...executionData
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при исполнении предложения по подписям:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ===== ИСТОРИЯ И СОБЫТИЯ =====
|
||||
|
||||
@@ -187,34 +111,9 @@ export const executeProposalBySignatures = async (dleAddress, executionData) =>
|
||||
* @param {number} toBlock - Конечный блок
|
||||
* @returns {Promise<Object>} - История событий
|
||||
*/
|
||||
export const getEventHistory = async (dleAddress, eventType, fromBlock, toBlock) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-event-history', {
|
||||
dleAddress,
|
||||
eventType,
|
||||
fromBlock,
|
||||
toBlock
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении истории событий:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает статистику DLE
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @returns {Promise<Object>} - Статистика
|
||||
*/
|
||||
export const getDLEStats = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-dle-stats', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении статистики DLE:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
// Сервис для работы с модулями DLE
|
||||
import axios from 'axios';
|
||||
import api from '@/api/axios';
|
||||
|
||||
/**
|
||||
* Создает предложение о добавлении модуля
|
||||
@@ -21,7 +21,7 @@ import axios from 'axios';
|
||||
*/
|
||||
export const createAddModuleProposal = async (dleAddress, moduleData) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-modules/create-add-module-proposal', {
|
||||
const response = await api.post('/dle-modules/create-add-module-proposal', {
|
||||
dleAddress,
|
||||
...moduleData
|
||||
});
|
||||
@@ -40,7 +40,7 @@ export const createAddModuleProposal = async (dleAddress, moduleData) => {
|
||||
*/
|
||||
export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-modules/create-remove-module-proposal', {
|
||||
const response = await api.post('/dle-modules/create-remove-module-proposal', {
|
||||
dleAddress,
|
||||
...moduleData
|
||||
});
|
||||
@@ -59,7 +59,7 @@ export const createRemoveModuleProposal = async (dleAddress, moduleData) => {
|
||||
*/
|
||||
export const isModuleActive = async (dleAddress, moduleId) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-modules/is-module-active', {
|
||||
const response = await api.post('/dle-modules/is-module-active', {
|
||||
dleAddress,
|
||||
moduleId
|
||||
});
|
||||
@@ -76,11 +76,12 @@ export const isModuleActive = async (dleAddress, moduleId) => {
|
||||
* @param {string} moduleId - ID модуля
|
||||
* @returns {Promise<Object>} - Адрес модуля
|
||||
*/
|
||||
export const getModuleAddress = async (dleAddress, moduleId) => {
|
||||
export const getModuleAddress = async (dleAddress, moduleId, chainId) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-modules/get-module-address', {
|
||||
const response = await api.post('/dle-modules/get-module-address', {
|
||||
dleAddress,
|
||||
moduleId
|
||||
moduleId,
|
||||
chainId
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -96,7 +97,7 @@ export const getModuleAddress = async (dleAddress, moduleId) => {
|
||||
*/
|
||||
export const getAllModules = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-modules/get-all-modules', {
|
||||
const response = await api.post('/dle-modules/get-all-modules', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
@@ -106,6 +107,23 @@ export const getAllModules = async (dleAddress) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает информацию о поддерживаемых сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @returns {Promise<Object>} - Информация о сетях
|
||||
*/
|
||||
export const getNetworksInfo = async (dleAddress) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/get-networks-info', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении информации о сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает информацию о модуле
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
@@ -114,7 +132,7 @@ export const getAllModules = async (dleAddress) => {
|
||||
*/
|
||||
export const getModuleInfo = async (dleAddress, moduleId) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-module-info', {
|
||||
const response = await api.post('/blockchain/get-module-info', {
|
||||
dleAddress,
|
||||
moduleId
|
||||
});
|
||||
@@ -132,7 +150,7 @@ export const getModuleInfo = async (dleAddress, moduleId) => {
|
||||
*/
|
||||
export const getModulesStats = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-modules-stats', {
|
||||
const response = await api.post('/blockchain/get-modules-stats', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
@@ -150,7 +168,7 @@ export const getModulesStats = async (dleAddress) => {
|
||||
*/
|
||||
export const getModulesHistory = async (dleAddress, filters = {}) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-modules-history', {
|
||||
const response = await api.post('/blockchain/get-modules-history', {
|
||||
dleAddress,
|
||||
...filters
|
||||
});
|
||||
@@ -168,7 +186,7 @@ export const getModulesHistory = async (dleAddress, filters = {}) => {
|
||||
*/
|
||||
export const getActiveModules = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-active-modules', {
|
||||
const response = await api.post('/blockchain/get-active-modules', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
@@ -185,7 +203,7 @@ export const getActiveModules = async (dleAddress) => {
|
||||
*/
|
||||
export const getInactiveModules = async (dleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-inactive-modules', {
|
||||
const response = await api.post('/blockchain/get-inactive-modules', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
@@ -204,7 +222,7 @@ export const getInactiveModules = async (dleAddress) => {
|
||||
*/
|
||||
export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/check-module-compatibility', {
|
||||
const response = await api.post('/blockchain/check-module-compatibility', {
|
||||
dleAddress,
|
||||
moduleId,
|
||||
moduleAddress
|
||||
@@ -224,7 +242,7 @@ export const checkModuleCompatibility = async (dleAddress, moduleId, moduleAddre
|
||||
*/
|
||||
export const getModuleConfig = async (dleAddress, moduleId) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-module-config', {
|
||||
const response = await api.post('/blockchain/get-module-config', {
|
||||
dleAddress,
|
||||
moduleId
|
||||
});
|
||||
@@ -244,7 +262,7 @@ export const getModuleConfig = async (dleAddress, moduleId) => {
|
||||
*/
|
||||
export const updateModuleConfig = async (dleAddress, moduleId, config) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/update-module-config', {
|
||||
const response = await api.post('/blockchain/update-module-config', {
|
||||
dleAddress,
|
||||
moduleId,
|
||||
config
|
||||
@@ -265,7 +283,7 @@ export const updateModuleConfig = async (dleAddress, moduleId, config) => {
|
||||
*/
|
||||
export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-module-events', {
|
||||
const response = await api.post('/blockchain/get-module-events', {
|
||||
dleAddress,
|
||||
moduleId,
|
||||
...filters
|
||||
@@ -285,7 +303,7 @@ export const getModuleEvents = async (dleAddress, moduleId, filters = {}) => {
|
||||
*/
|
||||
export const getModulePerformance = async (dleAddress, moduleId) => {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-module-performance', {
|
||||
const response = await api.post('/blockchain/get-module-performance', {
|
||||
dleAddress,
|
||||
moduleId
|
||||
});
|
||||
@@ -295,3 +313,231 @@ export const getModulePerformance = async (dleAddress, moduleId) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Инициализирует модули во всех сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} privateKey - Приватный ключ
|
||||
* @returns {Promise<Object>} - Результат инициализации
|
||||
*/
|
||||
export const initializeModulesAllNetworks = async (dleAddress, privateKey) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/initialize-modules-all-networks', {
|
||||
dleAddress,
|
||||
privateKey
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при инициализации модулей во всех сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Верифицирует модули во всех сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} privateKey - Приватный ключ
|
||||
* @returns {Promise<Object>} - Результат верификации
|
||||
*/
|
||||
export const verifyModulesAllNetworks = async (dleAddress, privateKey) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/verify-modules-all-networks', {
|
||||
dleAddress,
|
||||
privateKey
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при верификации модулей во всех сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверяет статус деплоя DLE контракта
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {Array<number>} chainIds - Список ID сетей
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Статус деплоя DLE
|
||||
*/
|
||||
export const checkDLEDeploymentStatus = async (dleAddress, chainIds, maxRetries = 3, retryDelay = 30000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/check-dle-deployment-status', {
|
||||
dleAddress,
|
||||
chainIds,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке статуса деплоя DLE:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверяет статус деплоя модуля
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||
* @param {Array<number>} chainIds - Список ID сетей
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Статус деплоя модуля
|
||||
*/
|
||||
export const checkModuleDeploymentStatus = async (dleAddress, moduleType, chainIds, maxRetries = 3, retryDelay = 30000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/check-module-deployment-status', {
|
||||
dleAddress,
|
||||
moduleType,
|
||||
chainIds,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при проверке статуса деплоя модуля:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Деплоит модуль во всех сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||
* @param {string} privateKey - Приватный ключ
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Результат деплоя модуля
|
||||
*/
|
||||
export const deployModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 45000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/deploy-module-all-networks', {
|
||||
dleAddress,
|
||||
moduleType,
|
||||
privateKey,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при деплое модуля во всех сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Верифицирует DLE контракт во всех сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} privateKey - Приватный ключ
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Результат верификации DLE
|
||||
*/
|
||||
export const verifyDLEAllNetworks = async (dleAddress, privateKey, maxRetries = 3, retryDelay = 60000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/verify-dle-all-networks', {
|
||||
dleAddress,
|
||||
privateKey,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при верификации DLE во всех сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Верифицирует модуль во всех сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||
* @param {string} privateKey - Приватный ключ
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Результат верификации модуля
|
||||
*/
|
||||
export const verifyModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 60000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/verify-module-all-networks', {
|
||||
dleAddress,
|
||||
moduleType,
|
||||
privateKey,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при верификации модуля во всех сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Инициализирует модуль во всех сетях
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {string} moduleType - Тип модуля (treasury, timelock, reader)
|
||||
* @param {string} privateKey - Приватный ключ
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Результат инициализации модуля
|
||||
*/
|
||||
export const initializeModuleAllNetworks = async (dleAddress, moduleType, privateKey, maxRetries = 3, retryDelay = 30000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/initialize-module-all-networks', {
|
||||
dleAddress,
|
||||
moduleType,
|
||||
privateKey,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при инициализации модуля во всех сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Выполняет финальную проверку готовности деплоя
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {Array<number>} chainIds - Список ID сетей
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Результат финальной проверки
|
||||
*/
|
||||
export const finalDeploymentCheck = async (dleAddress, chainIds, maxRetries = 3, retryDelay = 30000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/final-deployment-check', {
|
||||
dleAddress,
|
||||
chainIds,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при финальной проверке деплоя:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получает общий статус деплоя
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @param {number} maxRetries - Максимальное количество попыток
|
||||
* @param {number} retryDelay - Задержка между попытками (мс)
|
||||
* @returns {Promise<Object>} - Статус деплоя
|
||||
*/
|
||||
export const getDeploymentStatus = async (dleAddress, maxRetries = 3, retryDelay = 30000) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/get-deployment-status', {
|
||||
dleAddress,
|
||||
maxRetries,
|
||||
retryDelay
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении статуса деплоя:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -261,3 +261,20 @@ export const getQuorumAt = async (dleAddress, timepoint) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Декодирует данные предложения о добавлении модуля
|
||||
* @param {string} transactionHash - Хеш транзакции создания предложения
|
||||
* @returns {Promise<Object>} - Декодированные данные предложения
|
||||
*/
|
||||
export const decodeProposalData = async (transactionHash) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-proposals/decode-proposal-data', {
|
||||
transactionHash
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при декодировании данных предложения:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ class WebSocketService {
|
||||
try {
|
||||
// Определяем WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
// В Docker окружении используем тот же хост, что и для HTTP
|
||||
// Подключаемся к бэкенду через Vite proxy
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
// console.log('🔌 [WebSocket] Подключение к:', wsUrl);
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import axios from 'axios';
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import api from '@/api/axios';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
/**
|
||||
@@ -55,7 +67,7 @@ export async function checkWalletConnection() {
|
||||
*/
|
||||
export async function getDLEInfo(dleAddress) {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8000/api/blockchain/read-dle-info', {
|
||||
const response = await api.post('/blockchain/read-dle-info', {
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
@@ -232,7 +244,7 @@ export async function executeProposal(dleAddress, proposalId) {
|
||||
*/
|
||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/create-add-module-proposal', {
|
||||
const response = await api.post('/blockchain/create-add-module-proposal', {
|
||||
dleAddress: dleAddress,
|
||||
description: description,
|
||||
duration: duration,
|
||||
@@ -263,7 +275,7 @@ export async function createAddModuleProposal(dleAddress, description, duration,
|
||||
*/
|
||||
export async function createRemoveModuleProposal(dleAddress, description, duration, moduleId, chainId) {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/create-remove-module-proposal', {
|
||||
const response = await api.post('/blockchain/create-remove-module-proposal', {
|
||||
dleAddress: dleAddress,
|
||||
description: description,
|
||||
duration: duration,
|
||||
@@ -290,7 +302,7 @@ export async function createRemoveModuleProposal(dleAddress, description, durati
|
||||
*/
|
||||
export async function isModuleActive(dleAddress, moduleId) {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/is-module-active', {
|
||||
const response = await api.post('/blockchain/is-module-active', {
|
||||
dleAddress: dleAddress,
|
||||
moduleId: moduleId
|
||||
});
|
||||
@@ -312,11 +324,12 @@ export async function isModuleActive(dleAddress, moduleId) {
|
||||
* @param {string} moduleId - ID модуля
|
||||
* @returns {Promise<string>} - Адрес модуля
|
||||
*/
|
||||
export async function getModuleAddress(dleAddress, moduleId) {
|
||||
export async function getModuleAddress(dleAddress, moduleId, chainId) {
|
||||
try {
|
||||
const response = await axios.post('/dle-modules/get-module-address', {
|
||||
const response = await api.post('/dle-modules/get-module-address', {
|
||||
dleAddress: dleAddress,
|
||||
moduleId: moduleId
|
||||
moduleId: moduleId,
|
||||
chainId: chainId
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -338,7 +351,7 @@ export async function getModuleAddress(dleAddress, moduleId) {
|
||||
*/
|
||||
export async function isChainSupported(dleAddress, chainId) {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/is-chain-supported', {
|
||||
const response = await api.post('/blockchain/is-chain-supported', {
|
||||
dleAddress: dleAddress,
|
||||
chainId: chainId
|
||||
});
|
||||
@@ -361,7 +374,7 @@ export async function isChainSupported(dleAddress, chainId) {
|
||||
*/
|
||||
export async function getCurrentChainId(dleAddress) {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/get-current-chain-id', {
|
||||
const response = await api.post('/blockchain/get-current-chain-id', {
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
@@ -384,7 +397,7 @@ export async function getCurrentChainId(dleAddress) {
|
||||
*/
|
||||
export async function checkProposalResult(dleAddress, proposalId) {
|
||||
try {
|
||||
const response = await axios.post('/blockchain/check-proposal-result', {
|
||||
const response = await api.post('/blockchain/check-proposal-result', {
|
||||
dleAddress: dleAddress,
|
||||
proposalId: proposalId
|
||||
});
|
||||
@@ -410,7 +423,7 @@ export async function checkProposalResult(dleAddress, proposalId) {
|
||||
*/
|
||||
export async function loadProposals(dleAddress) {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8000/api/blockchain/get-proposals', {
|
||||
const response = await api.post('/blockchain/get-proposals', {
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
@@ -502,7 +515,7 @@ export async function loadAnalytics(dleAddress) {
|
||||
*/
|
||||
export async function getSupportedChains(dleAddress) {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8000/api/blockchain/get-supported-chains', {
|
||||
const response = await api.post('/blockchain/get-supported-chains', {
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
@@ -676,7 +689,7 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support)
|
||||
*/
|
||||
export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8000/api/blockchain/check-deactivation-proposal-result', {
|
||||
const response = await api.post('/blockchain/check-deactivation-proposal-result', {
|
||||
dleAddress: dleAddress,
|
||||
proposalId: proposalId
|
||||
});
|
||||
@@ -738,7 +751,7 @@ export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||
*/
|
||||
export async function loadDeactivationProposals(dleAddress) {
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8000/api/blockchain/load-deactivation-proposals', {
|
||||
const response = await api.post('/blockchain/load-deactivation-proposals', {
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* WebSocket клиент для автоматического обновления данных
|
||||
*/
|
||||
@@ -95,6 +107,23 @@ class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Алиас для on() - для совместимости с useDeploymentWebSocket
|
||||
subscribe(event, callback) {
|
||||
this.on(event, callback);
|
||||
}
|
||||
|
||||
// Алиас для off() - для совместимости с useDeploymentWebSocket
|
||||
unsubscribe(event, callback) {
|
||||
if (this.listeners.has(event)) {
|
||||
const callbacks = this.listeners.get(event);
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
callbacks.splice(index, 1);
|
||||
console.log(`[WebSocket] Отписались от события: ${event}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Отправка сообщения на сервер
|
||||
send(event, data) {
|
||||
if (this.ws && this.isConnected) {
|
||||
|
||||
@@ -827,14 +827,42 @@
|
||||
|
||||
<!-- Кнопка деплоя смарт-контрактов -->
|
||||
<div class="deploy-section">
|
||||
<!-- Информация о поэтапном деплое -->
|
||||
<div class="deployment-info">
|
||||
<h4>🚀 Поэтапный деплой DLE</h4>
|
||||
<p class="deployment-description">
|
||||
Автоматический деплой DLE контракта и всех модулей с проверками, верификацией и инициализацией во всех выбранных сетях
|
||||
</p>
|
||||
<div class="deployment-features">
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Деплой DLE контракта во всех сетях</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Автоматическая верификация контрактов</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Деплой и инициализация всех модулей</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Повторы при ошибках сети</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deploy-buttons">
|
||||
<button
|
||||
@click="deploySmartContracts"
|
||||
type="button"
|
||||
class="btn btn-primary btn-lg deploy-btn"
|
||||
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
|
||||
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
|
||||
>
|
||||
<i class="fas fa-rocket"></i> Деплой смарт контрактов
|
||||
<i class="fas fa-cogs"></i>
|
||||
Поэтапный деплой DLE
|
||||
</button>
|
||||
<button
|
||||
v-if="hasSelectedData"
|
||||
@@ -893,6 +921,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Мастер поэтапного деплоя -->
|
||||
<div v-if="showDeploymentWizard" class="deployment-wizard-overlay">
|
||||
<div class="wizard-container">
|
||||
<DeploymentWizard
|
||||
:private-key="unifiedPrivateKey"
|
||||
:selected-networks="selectedNetworks"
|
||||
:dle-data="dleSettings"
|
||||
:etherscan-api-key="etherscanApiKey"
|
||||
@deployment-completed="handleDeploymentCompleted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -900,10 +941,21 @@
|
||||
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import axios from 'axios';
|
||||
import api from '@/api/axios';
|
||||
import DeploymentWizard from '@/components/deployment/DeploymentWizard.vue';
|
||||
|
||||
const router = useRouter();
|
||||
// Нормализация приватного ключа: убираем пробелы/"0x", посторонние символы,
|
||||
// приводим к нижнему регистру и дополняем ведущими нулями до 64 символов
|
||||
function normalizePrivateKey(raw) {
|
||||
if (!raw || typeof raw !== 'string') return '';
|
||||
let pk = raw.trim().replace(/^0x/i, '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||
if (pk.length === 64) return '0x' + pk;
|
||||
if (pk.length > 64) return '';
|
||||
if (/^[0-9a-fA-F]*$/.test(pk)) return '0x' + pk.padStart(64, '0');
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
// Получаем контекст авторизации для адреса кошелька
|
||||
const { address, isAdmin } = useAuthContext();
|
||||
@@ -995,6 +1047,10 @@ const autoVerifyAfterDeploy = ref(true);
|
||||
// Состояние для приватных ключей
|
||||
const useSameKeyForAllChains = ref(true);
|
||||
const unifiedPrivateKey = ref('');
|
||||
|
||||
// Состояние мастера деплоя
|
||||
const showDeploymentWizard = ref(false);
|
||||
const deployedDLEAddress = ref('');
|
||||
const privateKeys = reactive({});
|
||||
const privateKeyVisibility = reactive({});
|
||||
const keyValidation = reactive({});
|
||||
@@ -1060,7 +1116,6 @@ const hasSelectedNetworks = computed(() => {
|
||||
// symbol: dleSettings.tokenSymbol,
|
||||
// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
|
||||
// };
|
||||
// const resp = await axios.post('/dle-v2/predict-addresses', payload);
|
||||
// if (resp.data && resp.data.success && resp.data.data) {
|
||||
// // ожидаем вид { [chainId]: address }
|
||||
// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
|
||||
@@ -1618,7 +1673,7 @@ const searchByPostalCode = async () => {
|
||||
}
|
||||
|
||||
// console.log(`[SearchByPostalCode] Querying Nominatim: ${params.toString()}`);
|
||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
// Преобразуем результаты Nominatim для отображения
|
||||
@@ -1757,7 +1812,7 @@ const verifyAddress = async () => {
|
||||
params.append('countrycodes', 'RU');
|
||||
}
|
||||
|
||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
const verificationResult = response.data[0];
|
||||
@@ -1833,7 +1888,7 @@ const formatTokenSymbol = () => {
|
||||
const loadCountries = async () => {
|
||||
isLoadingCountries.value = true;
|
||||
try {
|
||||
const response = await axios.get('/countries');
|
||||
const response = await api.get('/countries');
|
||||
if (response.data && response.data.success) {
|
||||
countriesOptions.value = response.data.data || [];
|
||||
console.log(`Загружено стран: ${countriesOptions.value.length}`);
|
||||
@@ -1857,7 +1912,7 @@ const loadRussianClassifiers = async () => {
|
||||
console.log('Загружаем российские классификаторы...');
|
||||
|
||||
// Загружаем все классификаторы одним запросом для оптимизации
|
||||
const response = await axios.get('/russian-classifiers/all');
|
||||
const response = await api.get('/russian-classifiers/all');
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const data = response.data.data;
|
||||
@@ -1905,7 +1960,7 @@ const loadKppCodes = async () => {
|
||||
|
||||
try {
|
||||
console.log('Загружаем КПП коды...');
|
||||
const response = await axios.get('/kpp/codes');
|
||||
const response = await api.get('/kpp/codes');
|
||||
|
||||
if (response.data && Array.isArray(response.data.codes)) {
|
||||
kppCodes.value = response.data.codes;
|
||||
@@ -1928,65 +1983,19 @@ const loadAvailableNetworks = async () => {
|
||||
|
||||
try {
|
||||
console.log('Загружаем доступные сети из базы данных...');
|
||||
const response = await axios.get('/settings/rpc');
|
||||
console.log('URL:', '/api/settings/rpc');
|
||||
const response = await api.get('/settings/rpc');
|
||||
console.log('Response:', response.data);
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const networksData = response.data.data || [];
|
||||
|
||||
// Преобразуем данные из базы в формат для мульти-чейн деплоя
|
||||
availableNetworks.value = networksData.map(network => {
|
||||
// Определяем примерную стоимость на основе chain_id
|
||||
const estimatedCosts = {
|
||||
1: 45.50, // Ethereum Mainnet
|
||||
137: 0.01, // Polygon
|
||||
42161: 2.30, // Arbitrum One
|
||||
10: 1.20, // Optimism
|
||||
56: 0.50, // BSC
|
||||
43114: 0.15, // Avalanche
|
||||
11155111: 0.001, // Sepolia testnet
|
||||
80001: 0.001, // Mumbai testnet
|
||||
421613: 0.001, // Arbitrum Goerli
|
||||
420: 0.001, // Optimism Goerli
|
||||
97: 0.001, // BSC Testnet
|
||||
43113: 0.001 // Avalanche Fuji
|
||||
};
|
||||
|
||||
// Определяем описания сетей
|
||||
const networkDescriptions = {
|
||||
1: 'Максимальная безопасность и децентрализация',
|
||||
137: 'Низкие комиссии, быстрые транзакции',
|
||||
42161: 'Оптимистичные rollups, средние комиссии',
|
||||
10: 'Оптимистичные rollups, низкие комиссии',
|
||||
56: 'Совместимость с экосистемой Binance',
|
||||
43114: 'Высокая пропускная способность',
|
||||
11155111: 'Тестовая сеть Ethereum',
|
||||
80001: 'Тестовая сеть Polygon',
|
||||
421613: 'Тестовая сеть Arbitrum',
|
||||
420: 'Тестовая сеть Optimism',
|
||||
97: 'Тестовая сеть BSC',
|
||||
43113: 'Тестовая сеть Avalanche'
|
||||
};
|
||||
|
||||
// Определяем названия сетей
|
||||
const networkNames = {
|
||||
1: 'Ethereum Mainnet',
|
||||
137: 'Polygon',
|
||||
42161: 'Arbitrum One',
|
||||
10: 'Optimism',
|
||||
56: 'BSC',
|
||||
43114: 'Avalanche',
|
||||
11155111: 'Sepolia Testnet',
|
||||
80001: 'Mumbai Testnet',
|
||||
421613: 'Arbitrum Goerli',
|
||||
420: 'Optimism Goerli',
|
||||
97: 'BSC Testnet',
|
||||
43113: 'Avalanche Fuji'
|
||||
};
|
||||
|
||||
const chainId = network.chain_id || parseInt(network.network_id);
|
||||
const estimatedCost = estimatedCosts[chainId] || 1.00;
|
||||
const description = networkDescriptions[chainId] || 'Блокчейн сеть';
|
||||
const name = networkNames[chainId] || network.network_id || 'Unknown Network';
|
||||
const chainId = network.chain_id || parseInt(network.network_id);
|
||||
const estimatedCost = getFallbackCost(chainId);
|
||||
const description = network.description || 'Блокчейн сеть';
|
||||
const name = network.name || network.network_id || `Chain ${chainId}`;
|
||||
|
||||
return {
|
||||
chainId: chainId,
|
||||
@@ -2042,7 +2051,7 @@ const validateTokenStandardCompatibility = () => {
|
||||
|
||||
// Проверяем совместимость ERC-4626 с тестовыми сетями
|
||||
if (standard === 'ERC4626') {
|
||||
const testnetChains = [11155111, 80001, 421613, 420, 97, 43113]; // Sepolia, Mumbai, etc.
|
||||
const testnetChains = [11155111, 80001, 421613, 420, 97]; // Sepolia, Mumbai, etc.
|
||||
const hasTestnet = networks.some(network => testnetChains.includes(network.chainId));
|
||||
|
||||
if (hasTestnet) {
|
||||
@@ -2075,12 +2084,80 @@ const showTokenStandardWarnings = () => {
|
||||
|
||||
// ==================== МУЛЬТИ-ЧЕЙН ФУНКЦИИ ====================
|
||||
|
||||
// Обновление общей стоимости деплоя
|
||||
const updateDeployCost = () => {
|
||||
totalDeployCost.value = selectedNetworkDetails.value
|
||||
.reduce((sum, network) => sum + network.estimatedCost, 0);
|
||||
// Обновление общей стоимости деплоя (динамический расчет)
|
||||
const updateDeployCost = async () => {
|
||||
if (selectedNetworkDetails.value.length === 0) {
|
||||
totalDeployCost.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем chainId выбранных сетей
|
||||
const chainIds = selectedNetworkDetails.value.map(network => network.chainId);
|
||||
|
||||
// Вызываем API для расчета стоимости
|
||||
const response = await api.post('/dle-v2/estimate-cost', {
|
||||
supportedChainIds: chainIds
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const costData = response.data.data;
|
||||
|
||||
// Обновляем информацию о каждой сети
|
||||
selectedNetworkDetails.value.forEach(network => {
|
||||
const estimate = costData.estimates.find(e => e.chainId === network.chainId);
|
||||
|
||||
if (estimate && estimate.ok) {
|
||||
network.estimatedCost = parseFloat(estimate.costEth);
|
||||
network.gasPrice = estimate.gasPrice;
|
||||
network.estimatedGas = estimate.gasLimit;
|
||||
} else {
|
||||
// Fallback для сетей без RPC
|
||||
network.estimatedCost = getFallbackCost(network.chainId);
|
||||
}
|
||||
});
|
||||
|
||||
totalDeployCost.value = parseFloat(costData.totalCostEth);
|
||||
console.log('✅ Стоимость деплоя обновлена:', costData);
|
||||
} else {
|
||||
throw new Error('Ошибка получения стоимости деплоя');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Ошибка расчета стоимости, используем fallback:', error.message);
|
||||
|
||||
// Fallback к статическим ценам
|
||||
selectedNetworkDetails.value.forEach(network => {
|
||||
network.estimatedCost = getFallbackCost(network.chainId);
|
||||
});
|
||||
|
||||
totalDeployCost.value = selectedNetworkDetails.value
|
||||
.reduce((sum, network) => sum + network.estimatedCost, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Вспомогательная функция для получения fallback стоимости
|
||||
const getFallbackCost = (chainId) => {
|
||||
const fallbackCosts = {
|
||||
1: 45.50, // Ethereum Mainnet
|
||||
137: 0.01, // Polygon
|
||||
42161: 2.30, // Arbitrum One
|
||||
10: 1.20, // Optimism
|
||||
56: 0.50, // BSC
|
||||
43114: 0.15, // Avalanche
|
||||
11155111: 0.001, // Sepolia testnet
|
||||
80001: 0.001, // Mumbai testnet
|
||||
421613: 0.001, // Arbitrum Goerli
|
||||
420: 0.001, // Optimism Goerli
|
||||
97: 0.001, // BSC Testnet
|
||||
17000: 0.001, // Holesky testnet
|
||||
421614: 0.001, // Arbitrum Sepolia
|
||||
84532: 0.001, // Base Sepolia
|
||||
80002: 0.001 // Polygon Amoy
|
||||
};
|
||||
return fallbackCosts[chainId] || 1.00;
|
||||
};
|
||||
|
||||
|
||||
// Копирование адреса DLE - отключено
|
||||
// const copyAddress = async () => {
|
||||
// try {
|
||||
@@ -2152,7 +2229,7 @@ const validatePrivateKey = async (chainId) => {
|
||||
|
||||
try {
|
||||
// Отправляем запрос на бэкенд для валидации
|
||||
const response = await axios.post('/dle-v2/validate-private-key', {
|
||||
const response = await api.post('/dle-v2/validate-private-key', {
|
||||
privateKey: key
|
||||
});
|
||||
|
||||
@@ -2275,12 +2352,14 @@ const handleVisibilityChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Watcher для unifiedPrivateKey с дебаунсом
|
||||
// Watcher: нормализуем PK и обновляем связанные состояния
|
||||
watch(unifiedPrivateKey, (newValue) => {
|
||||
// Добавляем небольшую задержку для предотвращения рекурсии
|
||||
setTimeout(() => {
|
||||
updateAllKeys();
|
||||
}, 100);
|
||||
const normalized = normalizePrivateKey(newValue);
|
||||
if (normalized && normalized !== newValue) {
|
||||
unifiedPrivateKey.value = normalized;
|
||||
return;
|
||||
}
|
||||
updateAllKeys();
|
||||
});
|
||||
|
||||
// Watcher для predictedAddress - синхронизация с dleSettings - отключено
|
||||
@@ -2309,6 +2388,11 @@ watch(unifiedPrivateKey, (newValue) => {
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
|
||||
// Сбрасываем состояние деплоя при загрузке страницы
|
||||
showDeployProgress.value = false;
|
||||
deployProgress.value = 0;
|
||||
deployStatus.value = '';
|
||||
|
||||
// Загружаем список стран
|
||||
loadCountries();
|
||||
|
||||
@@ -2337,6 +2421,11 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, есть ли приватный ключ
|
||||
if (!unifiedPrivateKey.value) {
|
||||
console.log('⚠️ Приватный ключ не введен. Пожалуйста, введите приватный ключ для деплоя.');
|
||||
}
|
||||
|
||||
// Добавляем слушатель события видимости страницы для обновления списка сетей
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
@@ -2367,23 +2456,22 @@ const checkAdminTokens = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
adminTokenCheck.value.isLoading = true;
|
||||
adminTokenCheck.value.error = null;
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: true, error: null };
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
|
||||
const response = await api.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
|
||||
|
||||
if (response.data.success) {
|
||||
adminTokenCheck.value.isAdmin = response.data.data.isAdmin;
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, isAdmin: response.data.data.isAdmin };
|
||||
console.log('Проверка админских токенов:', response.data.data);
|
||||
} else {
|
||||
adminTokenCheck.value.error = response.data.message || 'Ошибка проверки токенов';
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, error: response.data.message || 'Ошибка проверки токенов' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки админских токенов:', error);
|
||||
adminTokenCheck.value.error = error.response?.data?.message || 'Ошибка проверки токенов';
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, error: error.response?.data?.message || 'Ошибка проверки токенов' };
|
||||
} finally {
|
||||
adminTokenCheck.value.isLoading = false;
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: false };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2429,7 +2517,7 @@ const maskedPrivateKey = computed(() => {
|
||||
|
||||
// Функция деплоя смарт-контрактов DLE
|
||||
const deploySmartContracts = async () => {
|
||||
console.log('🚀 Начало деплоя DLE...');
|
||||
console.log('🚀 Начало поэтапного деплоя DLE...');
|
||||
try {
|
||||
// Валидация данных
|
||||
if (!isFormValid.value) {
|
||||
@@ -2437,12 +2525,33 @@ const deploySmartContracts = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сразу показываем мастер деплоя
|
||||
showDeploymentWizard.value = true;
|
||||
|
||||
// Запускаем деплой DLE в фоне
|
||||
startStagedDeployment();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка деплоя DLE:', error);
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция запуска поэтапного деплоя
|
||||
const startStagedDeployment = async () => {
|
||||
console.log('🚀 Запуск поэтапного деплоя...');
|
||||
|
||||
// Сначала выполняем стандартный деплой DLE контракта
|
||||
try {
|
||||
// Показываем индикатор процесса
|
||||
showDeployProgress.value = true;
|
||||
deployProgress.value = 10;
|
||||
deployStatus.value = 'Подготовка данных для деплоя...';
|
||||
deployStatus.value = 'Подготовка данных для деплоя DLE...';
|
||||
|
||||
// Подготовка данных для деплоя
|
||||
console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks);
|
||||
console.log('DEBUG: selectedNetworks.value:', selectedNetworks.value);
|
||||
const deployData = {
|
||||
// Основная информация DLE
|
||||
name: dleSettings.name,
|
||||
@@ -2463,16 +2572,15 @@ const deploySmartContracts = async () => {
|
||||
initialAmounts: dleSettings.partners.map(p => p.amount).filter(amount => amount > 0),
|
||||
|
||||
// Мульти-чейн настройки
|
||||
supportedChainIds: dleSettings.selectedNetworks || [],
|
||||
supportedChainIds: selectedNetworks.value || [],
|
||||
|
||||
// Текущая цепочка (будет установлена при деплое)
|
||||
currentChainId: dleSettings.selectedNetworks[0] || 1,
|
||||
|
||||
currentChainId: selectedNetworks.value[0] || 1,
|
||||
// Приватный ключ для деплоя
|
||||
privateKey: unifiedPrivateKey.value,
|
||||
// Верификация через Etherscan V2
|
||||
etherscanApiKey: etherscanApiKey.value,
|
||||
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
|
||||
autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя
|
||||
};
|
||||
|
||||
// Обработка логотипа
|
||||
@@ -2480,7 +2588,7 @@ const deploySmartContracts = async () => {
|
||||
if (logoFile.value) {
|
||||
const form = new FormData();
|
||||
form.append('logo', logoFile.value);
|
||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||
if (uploaded) {
|
||||
deployData.logoURI = uploaded;
|
||||
@@ -2488,162 +2596,113 @@ const deploySmartContracts = async () => {
|
||||
} else if (ensResolvedUrl.value) {
|
||||
deployData.logoURI = ensResolvedUrl.value;
|
||||
} else {
|
||||
// фолбэк на дефолт
|
||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Ошибка при обработке логотипа:', error.message);
|
||||
// Используем fallback логотип
|
||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||
}
|
||||
|
||||
console.log('Данные для деплоя DLE:', deployData);
|
||||
|
||||
// Предварительная проверка балансов во всех сетях
|
||||
// Предварительная проверка балансов (через приватный ключ)
|
||||
deployProgress.value = 20;
|
||||
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
|
||||
try {
|
||||
const pre = await axios.post('/dle-v2/precheck', {
|
||||
const pre = await api.post('/dle-v2/precheck', {
|
||||
supportedChainIds: deployData.supportedChainIds,
|
||||
privateKey: deployData.privateKey
|
||||
privateKey: unifiedPrivateKey.value
|
||||
});
|
||||
const preData = pre.data?.data;
|
||||
if (pre.data?.success && preData) {
|
||||
const lacks = (preData.insufficient || []);
|
||||
const warnings = (preData.warnings || []);
|
||||
|
||||
if (lacks.length > 0) {
|
||||
const lines = (preData.balances || []).map(b => {
|
||||
const status = b.ok ? '✅' : '❌';
|
||||
const warning = warnings.includes(b.chainId) ? ' ⚠️' : '';
|
||||
return `${status} Chain ${b.chainId}: ${b.balanceEth} ETH (мин. ${b.minRequiredEth} ETH)${warning}`;
|
||||
});
|
||||
|
||||
const message = `Проверка балансов завершена:\n\n${lines.join('\n')}\n\n${lacks.length > 0 ? '❌ Недостаточно средств в некоторых сетях!' : ''}\n${warnings.length > 0 ? '⚠️ Предупреждения в некоторых сетях!' : ''}`;
|
||||
|
||||
if (lacks.length > 0) {
|
||||
alert(message);
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
} else if (warnings.length > 0) {
|
||||
const proceed = confirm(message + '\n\nПродолжить деплой?');
|
||||
if (!proceed) {
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const message = `❌ Недостаточно средств в некоторых сетях!`;
|
||||
alert(message);
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Проверка балансов пройдена:', preData.summary);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Ошибка проверки балансов:', e.message);
|
||||
// Если precheck недоступен, не блокируем — продолжаем
|
||||
}
|
||||
|
||||
deployProgress.value = 30;
|
||||
deployStatus.value = 'Компиляция смарт-контрактов...';
|
||||
|
||||
// Автокомпиляция контрактов перед деплоем
|
||||
console.log('🔨 Запуск автокомпиляции...');
|
||||
// Автокомпиляция контрактов
|
||||
try {
|
||||
const compileResponse = await axios.post('/compile-contracts');
|
||||
const compileResponse = await api.post('/compile-contracts');
|
||||
console.log('✅ Контракты скомпилированы:', compileResponse.data);
|
||||
} catch (compileError) {
|
||||
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
|
||||
// Продолжаем деплой даже если компиляция не удалась
|
||||
}
|
||||
|
||||
deployProgress.value = 40;
|
||||
deployStatus.value = 'Отправка данных на сервер...';
|
||||
deployStatus.value = 'Деплой DLE контракта...';
|
||||
|
||||
// Вызов API для деплоя
|
||||
deployProgress.value = 50;
|
||||
deployStatus.value = 'Деплой смарт-контракта в блокчейне...';
|
||||
|
||||
const response = await axios.post('/dle-v2', deployData);
|
||||
|
||||
// Деплой будет выполнен в DeploymentWizard
|
||||
// Здесь только показываем мастер деплоя
|
||||
deployProgress.value = 80;
|
||||
deployStatus.value = 'Проверка результатов деплоя...';
|
||||
deployStatus.value = 'Запуск мастера деплоя...';
|
||||
|
||||
if (response.data.success) {
|
||||
const result = response.data.data;
|
||||
|
||||
// Проверяем результаты мульти-чейн деплоя
|
||||
if (result.networks && Array.isArray(result.networks)) {
|
||||
const successfulNetworks = result.networks.filter(n => n.success);
|
||||
const failedNetworks = result.networks.filter(n => !n.success);
|
||||
|
||||
if (failedNetworks.length > 0) {
|
||||
console.warn('Некоторые сети не удалось развернуть:', failedNetworks);
|
||||
}
|
||||
|
||||
if (successfulNetworks.length > 0) {
|
||||
// Проверяем, что все адреса одинаковые
|
||||
const addresses = successfulNetworks.map(n => n.address);
|
||||
const uniqueAddresses = [...new Set(addresses)];
|
||||
|
||||
if (uniqueAddresses.length === 1) {
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = `✅ DLE успешно развернут в ${successfulNetworks.length} сетях с одинаковым адресом!`;
|
||||
|
||||
console.log('🎉 Мульти-чейн деплой завершен успешно!');
|
||||
console.log('Адрес DLE:', uniqueAddresses[0]);
|
||||
console.log('Сети:', successfulNetworks.map(n => `Chain ${n.chainId}: ${n.address}`));
|
||||
|
||||
// Небольшая задержка для показа успешного завершения
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
}, 3000);
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ ОШИБКА: Адреса DLE в разных сетях не совпадают! Это может указывать на проблему с CREATE2.');
|
||||
}
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Не удалось развернуть DLE ни в одной сети');
|
||||
}
|
||||
} else {
|
||||
// Fallback для одиночного деплоя
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = '✅ DLE успешно развернут!';
|
||||
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
router.push('/management');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое: ' + (response.data.message || response.data.error));
|
||||
}
|
||||
// Показываем мастер деплоя
|
||||
showDeploymentWizard.value = true;
|
||||
|
||||
// Мастер деплоя сам выполнит деплой
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Ошибка деплоя DLE:', error);
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
||||
console.error('Ошибка при запуске деплоя:', error);
|
||||
deployStatus.value = `❌ Ошибка: ${error.message}`;
|
||||
deployProgress.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик завершения поэтапного деплоя
|
||||
const handleDeploymentCompleted = (result) => {
|
||||
console.log('🎉 Поэтапный деплой завершен:', result);
|
||||
showDeploymentWizard.value = false;
|
||||
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
};
|
||||
|
||||
// Валидация формы
|
||||
const isFormValid = computed(() => {
|
||||
const isFormValid = computed(() => {
|
||||
const validation = {
|
||||
jurisdiction: !!dleSettings.jurisdiction,
|
||||
name: !!dleSettings.name,
|
||||
tokenSymbol: !!dleSettings.tokenSymbol,
|
||||
partners: dleSettings.partners.length > 0,
|
||||
partnersValid: dleSettings.partners.every(partner => partner.address && partner.amount > 0),
|
||||
quorum: dleSettings.governanceQuorum > 0 && dleSettings.governanceQuorum <= 100,
|
||||
networks: selectedNetworks.value.length > 0,
|
||||
privateKey: !!unifiedPrivateKey.value,
|
||||
keyValid: !!keyValidation.unified?.isValid,
|
||||
coordinates: validateCoordinates(dleSettings.coordinates)
|
||||
};
|
||||
|
||||
console.log('🔍 Валидация формы:', validation);
|
||||
console.log('🔍 selectedNetworks.value:', selectedNetworks.value);
|
||||
console.log('🔍 adminTokenCheck:', adminTokenCheck.value);
|
||||
console.log('🔍 showDeployProgress:', showDeployProgress.value);
|
||||
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
|
||||
console.log('🔍 keyValidation.unified:', keyValidation.unified);
|
||||
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates);
|
||||
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading && !showDeployProgress.value);
|
||||
|
||||
return Boolean(
|
||||
dleSettings.jurisdiction &&
|
||||
dleSettings.name &&
|
||||
dleSettings.tokenSymbol &&
|
||||
(dleSettings.partners.length > 0) &&
|
||||
dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
|
||||
dleSettings.governanceQuorum > 0 &&
|
||||
dleSettings.governanceQuorum <= 100 &&
|
||||
(dleSettings.selectedNetworks.length > 0) &&
|
||||
// Проверка приватного ключа
|
||||
unifiedPrivateKey.value &&
|
||||
keyValidation.unified?.isValid &&
|
||||
// Валидация координат
|
||||
validateCoordinates(dleSettings.coordinates)
|
||||
validation.jurisdiction &&
|
||||
validation.name &&
|
||||
validation.tokenSymbol &&
|
||||
validation.partners &&
|
||||
validation.partnersValid &&
|
||||
validation.quorum &&
|
||||
validation.networks &&
|
||||
validation.privateKey &&
|
||||
validation.keyValid &&
|
||||
validation.coordinates
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2715,7 +2774,7 @@ async function submitDeploy() {
|
||||
if (logoFile.value) {
|
||||
const form = new FormData();
|
||||
form.append('logo', logoFile.value);
|
||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||
if (uploaded) {
|
||||
deployData.logoURI = uploaded;
|
||||
@@ -4385,6 +4444,85 @@ async function submitDeploy() {
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Стили для информации о деплое */
|
||||
.deployment-info {
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.deployment-info h4 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.deployment-description {
|
||||
color: #6c757d;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.deployment-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.feature-item i {
|
||||
color: #28a745;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.feature-item span {
|
||||
color: #495057;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Стили для мастера деплоя */
|
||||
.deployment-wizard-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wizard-container {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.deploy-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
27
frontend/src/views/smartcontracts/modules/index.js
Normal file
27
frontend/src/views/smartcontracts/modules/index.js
Normal 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';
|
||||
Reference in New Issue
Block a user