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

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

View File

@@ -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' &&

View 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>

View File

@@ -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 }
]

View 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
};
}

View File

@@ -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',

View File

@@ -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;
}
};

View File

@@ -11,7 +11,7 @@
*/
// Сервис для работы с модулями DLE
import axios from 'axios';
import api from '@/api/axios';
/**
* Создает предложение о добавлении модуля
@@ -21,7 +21,7 @@ import axios from 'axios';
*/
export const 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;
}
};

View File

@@ -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;
}
};

View File

@@ -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);

View File

@@ -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
});

View File

@@ -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) {

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
// Основные модули DLE
export { default as TreasuryModuleDeployView } from './TreasuryModuleDeployView.vue';
export { default as TimelockModuleDeployView } from './TimelockModuleDeployView.vue';
export { default as DLEReaderDeployView } from './DLEReaderDeployView.vue';
// Дополнительные модули
export { default as CommunicationModuleDeployView } from './CommunicationModuleDeployView.vue';
export { default as ApplicationModuleDeployView } from './ApplicationModuleDeployView.vue';
export { default as MintModuleDeploy } from './MintModuleDeploy.vue';
export { default as BurnModuleDeploy } from './BurnModuleDeploy.vue';
export { default as OracleModuleDeploy } from './OracleModuleDeploy.vue';
export { default as InheritanceModuleDeploy } from './InheritanceModuleDeploy.vue';
// Кастомный модуль
export { default as ModuleDeployFormView } from './ModuleDeployFormView.vue';