feat: Добавлены формы деплоя модулей DLE с полными настройками
- Создана форма деплоя TreasuryModule с детальными настройками казны - Создана форма деплоя TimelockModule с настройками временных задержек - Создана форма деплоя DLEReader с простой конфигурацией - Добавлены маршруты и индексы для всех модулей - Исправлены пути импорта BaseLayout - Добавлены авторские права во все файлы - Улучшена архитектура деплоя модулей отдельно от основного DLE
This commit is contained in:
@@ -827,14 +827,42 @@
|
||||
|
||||
<!-- Кнопка деплоя смарт-контрактов -->
|
||||
<div class="deploy-section">
|
||||
<!-- Информация о поэтапном деплое -->
|
||||
<div class="deployment-info">
|
||||
<h4>🚀 Поэтапный деплой DLE</h4>
|
||||
<p class="deployment-description">
|
||||
Автоматический деплой DLE контракта и всех модулей с проверками, верификацией и инициализацией во всех выбранных сетях
|
||||
</p>
|
||||
<div class="deployment-features">
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Деплой DLE контракта во всех сетях</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Автоматическая верификация контрактов</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Деплой и инициализация всех модулей</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>Повторы при ошибках сети</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="deploy-buttons">
|
||||
<button
|
||||
@click="deploySmartContracts"
|
||||
type="button"
|
||||
class="btn btn-primary btn-lg deploy-btn"
|
||||
:disabled="!isFormValid || !adminTokenCheck.isAdmin || adminTokenCheck.isLoading || showDeployProgress"
|
||||
:title="`isFormValid: ${isFormValid}, isAdmin: ${adminTokenCheck.isAdmin}, isLoading: ${adminTokenCheck.isLoading}, showDeployProgress: ${showDeployProgress}`"
|
||||
>
|
||||
<i class="fas fa-rocket"></i> Деплой смарт контрактов
|
||||
<i class="fas fa-cogs"></i>
|
||||
Поэтапный деплой DLE
|
||||
</button>
|
||||
<button
|
||||
v-if="hasSelectedData"
|
||||
@@ -893,6 +921,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Мастер поэтапного деплоя -->
|
||||
<div v-if="showDeploymentWizard" class="deployment-wizard-overlay">
|
||||
<div class="wizard-container">
|
||||
<DeploymentWizard
|
||||
:private-key="unifiedPrivateKey"
|
||||
:selected-networks="selectedNetworks"
|
||||
:dle-data="dleSettings"
|
||||
:etherscan-api-key="etherscanApiKey"
|
||||
@deployment-completed="handleDeploymentCompleted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -900,10 +941,21 @@
|
||||
import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import axios from 'axios';
|
||||
import api from '@/api/axios';
|
||||
import DeploymentWizard from '@/components/deployment/DeploymentWizard.vue';
|
||||
|
||||
const router = useRouter();
|
||||
// Нормализация приватного ключа: убираем пробелы/"0x", посторонние символы,
|
||||
// приводим к нижнему регистру и дополняем ведущими нулями до 64 символов
|
||||
function normalizePrivateKey(raw) {
|
||||
if (!raw || typeof raw !== 'string') return '';
|
||||
let pk = raw.trim().replace(/^0x/i, '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
||||
if (pk.length === 64) return '0x' + pk;
|
||||
if (pk.length > 64) return '';
|
||||
if (/^[0-9a-fA-F]*$/.test(pk)) return '0x' + pk.padStart(64, '0');
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
// Получаем контекст авторизации для адреса кошелька
|
||||
const { address, isAdmin } = useAuthContext();
|
||||
@@ -995,6 +1047,10 @@ const autoVerifyAfterDeploy = ref(true);
|
||||
// Состояние для приватных ключей
|
||||
const useSameKeyForAllChains = ref(true);
|
||||
const unifiedPrivateKey = ref('');
|
||||
|
||||
// Состояние мастера деплоя
|
||||
const showDeploymentWizard = ref(false);
|
||||
const deployedDLEAddress = ref('');
|
||||
const privateKeys = reactive({});
|
||||
const privateKeyVisibility = reactive({});
|
||||
const keyValidation = reactive({});
|
||||
@@ -1060,7 +1116,6 @@ const hasSelectedNetworks = computed(() => {
|
||||
// symbol: dleSettings.tokenSymbol,
|
||||
// selectedNetworks: selectedNetworkDetails.value.map(n => n.chainId)
|
||||
// };
|
||||
// const resp = await axios.post('/dle-v2/predict-addresses', payload);
|
||||
// if (resp.data && resp.data.success && resp.data.data) {
|
||||
// // ожидаем вид { [chainId]: address }
|
||||
// Object.keys(predictedAddresses).forEach(k => delete predictedAddresses[k]);
|
||||
@@ -1618,7 +1673,7 @@ const searchByPostalCode = async () => {
|
||||
}
|
||||
|
||||
// console.log(`[SearchByPostalCode] Querying Nominatim: ${params.toString()}`);
|
||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
// Преобразуем результаты Nominatim для отображения
|
||||
@@ -1757,7 +1812,7 @@ const verifyAddress = async () => {
|
||||
params.append('countrycodes', 'RU');
|
||||
}
|
||||
|
||||
const response = await axios.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
const response = await api.get(`/geocoding/nominatim-search?${params.toString()}`);
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
const verificationResult = response.data[0];
|
||||
@@ -1833,7 +1888,7 @@ const formatTokenSymbol = () => {
|
||||
const loadCountries = async () => {
|
||||
isLoadingCountries.value = true;
|
||||
try {
|
||||
const response = await axios.get('/countries');
|
||||
const response = await api.get('/countries');
|
||||
if (response.data && response.data.success) {
|
||||
countriesOptions.value = response.data.data || [];
|
||||
console.log(`Загружено стран: ${countriesOptions.value.length}`);
|
||||
@@ -1857,7 +1912,7 @@ const loadRussianClassifiers = async () => {
|
||||
console.log('Загружаем российские классификаторы...');
|
||||
|
||||
// Загружаем все классификаторы одним запросом для оптимизации
|
||||
const response = await axios.get('/russian-classifiers/all');
|
||||
const response = await api.get('/russian-classifiers/all');
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const data = response.data.data;
|
||||
@@ -1905,7 +1960,7 @@ const loadKppCodes = async () => {
|
||||
|
||||
try {
|
||||
console.log('Загружаем КПП коды...');
|
||||
const response = await axios.get('/kpp/codes');
|
||||
const response = await api.get('/kpp/codes');
|
||||
|
||||
if (response.data && Array.isArray(response.data.codes)) {
|
||||
kppCodes.value = response.data.codes;
|
||||
@@ -1928,65 +1983,19 @@ const loadAvailableNetworks = async () => {
|
||||
|
||||
try {
|
||||
console.log('Загружаем доступные сети из базы данных...');
|
||||
const response = await axios.get('/settings/rpc');
|
||||
console.log('URL:', '/api/settings/rpc');
|
||||
const response = await api.get('/settings/rpc');
|
||||
console.log('Response:', response.data);
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const networksData = response.data.data || [];
|
||||
|
||||
// Преобразуем данные из базы в формат для мульти-чейн деплоя
|
||||
availableNetworks.value = networksData.map(network => {
|
||||
// Определяем примерную стоимость на основе chain_id
|
||||
const estimatedCosts = {
|
||||
1: 45.50, // Ethereum Mainnet
|
||||
137: 0.01, // Polygon
|
||||
42161: 2.30, // Arbitrum One
|
||||
10: 1.20, // Optimism
|
||||
56: 0.50, // BSC
|
||||
43114: 0.15, // Avalanche
|
||||
11155111: 0.001, // Sepolia testnet
|
||||
80001: 0.001, // Mumbai testnet
|
||||
421613: 0.001, // Arbitrum Goerli
|
||||
420: 0.001, // Optimism Goerli
|
||||
97: 0.001, // BSC Testnet
|
||||
43113: 0.001 // Avalanche Fuji
|
||||
};
|
||||
|
||||
// Определяем описания сетей
|
||||
const networkDescriptions = {
|
||||
1: 'Максимальная безопасность и децентрализация',
|
||||
137: 'Низкие комиссии, быстрые транзакции',
|
||||
42161: 'Оптимистичные rollups, средние комиссии',
|
||||
10: 'Оптимистичные rollups, низкие комиссии',
|
||||
56: 'Совместимость с экосистемой Binance',
|
||||
43114: 'Высокая пропускная способность',
|
||||
11155111: 'Тестовая сеть Ethereum',
|
||||
80001: 'Тестовая сеть Polygon',
|
||||
421613: 'Тестовая сеть Arbitrum',
|
||||
420: 'Тестовая сеть Optimism',
|
||||
97: 'Тестовая сеть BSC',
|
||||
43113: 'Тестовая сеть Avalanche'
|
||||
};
|
||||
|
||||
// Определяем названия сетей
|
||||
const networkNames = {
|
||||
1: 'Ethereum Mainnet',
|
||||
137: 'Polygon',
|
||||
42161: 'Arbitrum One',
|
||||
10: 'Optimism',
|
||||
56: 'BSC',
|
||||
43114: 'Avalanche',
|
||||
11155111: 'Sepolia Testnet',
|
||||
80001: 'Mumbai Testnet',
|
||||
421613: 'Arbitrum Goerli',
|
||||
420: 'Optimism Goerli',
|
||||
97: 'BSC Testnet',
|
||||
43113: 'Avalanche Fuji'
|
||||
};
|
||||
|
||||
const chainId = network.chain_id || parseInt(network.network_id);
|
||||
const estimatedCost = estimatedCosts[chainId] || 1.00;
|
||||
const description = networkDescriptions[chainId] || 'Блокчейн сеть';
|
||||
const name = networkNames[chainId] || network.network_id || 'Unknown Network';
|
||||
const chainId = network.chain_id || parseInt(network.network_id);
|
||||
const estimatedCost = getFallbackCost(chainId);
|
||||
const description = network.description || 'Блокчейн сеть';
|
||||
const name = network.name || network.network_id || `Chain ${chainId}`;
|
||||
|
||||
return {
|
||||
chainId: chainId,
|
||||
@@ -2042,7 +2051,7 @@ const validateTokenStandardCompatibility = () => {
|
||||
|
||||
// Проверяем совместимость ERC-4626 с тестовыми сетями
|
||||
if (standard === 'ERC4626') {
|
||||
const testnetChains = [11155111, 80001, 421613, 420, 97, 43113]; // Sepolia, Mumbai, etc.
|
||||
const testnetChains = [11155111, 80001, 421613, 420, 97]; // Sepolia, Mumbai, etc.
|
||||
const hasTestnet = networks.some(network => testnetChains.includes(network.chainId));
|
||||
|
||||
if (hasTestnet) {
|
||||
@@ -2075,12 +2084,80 @@ const showTokenStandardWarnings = () => {
|
||||
|
||||
// ==================== МУЛЬТИ-ЧЕЙН ФУНКЦИИ ====================
|
||||
|
||||
// Обновление общей стоимости деплоя
|
||||
const updateDeployCost = () => {
|
||||
totalDeployCost.value = selectedNetworkDetails.value
|
||||
.reduce((sum, network) => sum + network.estimatedCost, 0);
|
||||
// Обновление общей стоимости деплоя (динамический расчет)
|
||||
const updateDeployCost = async () => {
|
||||
if (selectedNetworkDetails.value.length === 0) {
|
||||
totalDeployCost.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Получаем chainId выбранных сетей
|
||||
const chainIds = selectedNetworkDetails.value.map(network => network.chainId);
|
||||
|
||||
// Вызываем API для расчета стоимости
|
||||
const response = await api.post('/dle-v2/estimate-cost', {
|
||||
supportedChainIds: chainIds
|
||||
});
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const costData = response.data.data;
|
||||
|
||||
// Обновляем информацию о каждой сети
|
||||
selectedNetworkDetails.value.forEach(network => {
|
||||
const estimate = costData.estimates.find(e => e.chainId === network.chainId);
|
||||
|
||||
if (estimate && estimate.ok) {
|
||||
network.estimatedCost = parseFloat(estimate.costEth);
|
||||
network.gasPrice = estimate.gasPrice;
|
||||
network.estimatedGas = estimate.gasLimit;
|
||||
} else {
|
||||
// Fallback для сетей без RPC
|
||||
network.estimatedCost = getFallbackCost(network.chainId);
|
||||
}
|
||||
});
|
||||
|
||||
totalDeployCost.value = parseFloat(costData.totalCostEth);
|
||||
console.log('✅ Стоимость деплоя обновлена:', costData);
|
||||
} else {
|
||||
throw new Error('Ошибка получения стоимости деплоя');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Ошибка расчета стоимости, используем fallback:', error.message);
|
||||
|
||||
// Fallback к статическим ценам
|
||||
selectedNetworkDetails.value.forEach(network => {
|
||||
network.estimatedCost = getFallbackCost(network.chainId);
|
||||
});
|
||||
|
||||
totalDeployCost.value = selectedNetworkDetails.value
|
||||
.reduce((sum, network) => sum + network.estimatedCost, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// Вспомогательная функция для получения fallback стоимости
|
||||
const getFallbackCost = (chainId) => {
|
||||
const fallbackCosts = {
|
||||
1: 45.50, // Ethereum Mainnet
|
||||
137: 0.01, // Polygon
|
||||
42161: 2.30, // Arbitrum One
|
||||
10: 1.20, // Optimism
|
||||
56: 0.50, // BSC
|
||||
43114: 0.15, // Avalanche
|
||||
11155111: 0.001, // Sepolia testnet
|
||||
80001: 0.001, // Mumbai testnet
|
||||
421613: 0.001, // Arbitrum Goerli
|
||||
420: 0.001, // Optimism Goerli
|
||||
97: 0.001, // BSC Testnet
|
||||
17000: 0.001, // Holesky testnet
|
||||
421614: 0.001, // Arbitrum Sepolia
|
||||
84532: 0.001, // Base Sepolia
|
||||
80002: 0.001 // Polygon Amoy
|
||||
};
|
||||
return fallbackCosts[chainId] || 1.00;
|
||||
};
|
||||
|
||||
|
||||
// Копирование адреса DLE - отключено
|
||||
// const copyAddress = async () => {
|
||||
// try {
|
||||
@@ -2152,7 +2229,7 @@ const validatePrivateKey = async (chainId) => {
|
||||
|
||||
try {
|
||||
// Отправляем запрос на бэкенд для валидации
|
||||
const response = await axios.post('/dle-v2/validate-private-key', {
|
||||
const response = await api.post('/dle-v2/validate-private-key', {
|
||||
privateKey: key
|
||||
});
|
||||
|
||||
@@ -2275,12 +2352,14 @@ const handleVisibilityChange = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Watcher для unifiedPrivateKey с дебаунсом
|
||||
// Watcher: нормализуем PK и обновляем связанные состояния
|
||||
watch(unifiedPrivateKey, (newValue) => {
|
||||
// Добавляем небольшую задержку для предотвращения рекурсии
|
||||
setTimeout(() => {
|
||||
updateAllKeys();
|
||||
}, 100);
|
||||
const normalized = normalizePrivateKey(newValue);
|
||||
if (normalized && normalized !== newValue) {
|
||||
unifiedPrivateKey.value = normalized;
|
||||
return;
|
||||
}
|
||||
updateAllKeys();
|
||||
});
|
||||
|
||||
// Watcher для predictedAddress - синхронизация с dleSettings - отключено
|
||||
@@ -2309,6 +2388,11 @@ watch(unifiedPrivateKey, (newValue) => {
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
|
||||
// Сбрасываем состояние деплоя при загрузке страницы
|
||||
showDeployProgress.value = false;
|
||||
deployProgress.value = 0;
|
||||
deployStatus.value = '';
|
||||
|
||||
// Загружаем список стран
|
||||
loadCountries();
|
||||
|
||||
@@ -2337,6 +2421,11 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем, есть ли приватный ключ
|
||||
if (!unifiedPrivateKey.value) {
|
||||
console.log('⚠️ Приватный ключ не введен. Пожалуйста, введите приватный ключ для деплоя.');
|
||||
}
|
||||
|
||||
// Добавляем слушатель события видимости страницы для обновления списка сетей
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
@@ -2367,23 +2456,22 @@ const checkAdminTokens = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
adminTokenCheck.value.isLoading = true;
|
||||
adminTokenCheck.value.error = null;
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: true, error: null };
|
||||
|
||||
try {
|
||||
const response = await axios.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
|
||||
const response = await api.get(`/dle-v2/check-admin-tokens?address=${address.value}`);
|
||||
|
||||
if (response.data.success) {
|
||||
adminTokenCheck.value.isAdmin = response.data.data.isAdmin;
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, isAdmin: response.data.data.isAdmin };
|
||||
console.log('Проверка админских токенов:', response.data.data);
|
||||
} else {
|
||||
adminTokenCheck.value.error = response.data.message || 'Ошибка проверки токенов';
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, error: response.data.message || 'Ошибка проверки токенов' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки админских токенов:', error);
|
||||
adminTokenCheck.value.error = error.response?.data?.message || 'Ошибка проверки токенов';
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, error: error.response?.data?.message || 'Ошибка проверки токенов' };
|
||||
} finally {
|
||||
adminTokenCheck.value.isLoading = false;
|
||||
adminTokenCheck.value = { ...adminTokenCheck.value, isLoading: false };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2429,7 +2517,7 @@ const maskedPrivateKey = computed(() => {
|
||||
|
||||
// Функция деплоя смарт-контрактов DLE
|
||||
const deploySmartContracts = async () => {
|
||||
console.log('🚀 Начало деплоя DLE...');
|
||||
console.log('🚀 Начало поэтапного деплоя DLE...');
|
||||
try {
|
||||
// Валидация данных
|
||||
if (!isFormValid.value) {
|
||||
@@ -2437,12 +2525,33 @@ const deploySmartContracts = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сразу показываем мастер деплоя
|
||||
showDeploymentWizard.value = true;
|
||||
|
||||
// Запускаем деплой DLE в фоне
|
||||
startStagedDeployment();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка деплоя DLE:', error);
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Функция запуска поэтапного деплоя
|
||||
const startStagedDeployment = async () => {
|
||||
console.log('🚀 Запуск поэтапного деплоя...');
|
||||
|
||||
// Сначала выполняем стандартный деплой DLE контракта
|
||||
try {
|
||||
// Показываем индикатор процесса
|
||||
showDeployProgress.value = true;
|
||||
deployProgress.value = 10;
|
||||
deployStatus.value = 'Подготовка данных для деплоя...';
|
||||
deployStatus.value = 'Подготовка данных для деплоя DLE...';
|
||||
|
||||
// Подготовка данных для деплоя
|
||||
console.log('DEBUG: dleSettings.selectedNetworks:', dleSettings.selectedNetworks);
|
||||
console.log('DEBUG: selectedNetworks.value:', selectedNetworks.value);
|
||||
const deployData = {
|
||||
// Основная информация DLE
|
||||
name: dleSettings.name,
|
||||
@@ -2463,16 +2572,15 @@ const deploySmartContracts = async () => {
|
||||
initialAmounts: dleSettings.partners.map(p => p.amount).filter(amount => amount > 0),
|
||||
|
||||
// Мульти-чейн настройки
|
||||
supportedChainIds: dleSettings.selectedNetworks || [],
|
||||
supportedChainIds: selectedNetworks.value || [],
|
||||
|
||||
// Текущая цепочка (будет установлена при деплое)
|
||||
currentChainId: dleSettings.selectedNetworks[0] || 1,
|
||||
|
||||
currentChainId: selectedNetworks.value[0] || 1,
|
||||
// Приватный ключ для деплоя
|
||||
privateKey: unifiedPrivateKey.value,
|
||||
// Верификация через Etherscan V2
|
||||
etherscanApiKey: etherscanApiKey.value,
|
||||
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
|
||||
autoVerifyAfterDeploy: false // Отключаем автоверификацию для поэтапного деплоя
|
||||
};
|
||||
|
||||
// Обработка логотипа
|
||||
@@ -2480,7 +2588,7 @@ const deploySmartContracts = async () => {
|
||||
if (logoFile.value) {
|
||||
const form = new FormData();
|
||||
form.append('logo', logoFile.value);
|
||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||
if (uploaded) {
|
||||
deployData.logoURI = uploaded;
|
||||
@@ -2488,162 +2596,113 @@ const deploySmartContracts = async () => {
|
||||
} else if (ensResolvedUrl.value) {
|
||||
deployData.logoURI = ensResolvedUrl.value;
|
||||
} else {
|
||||
// фолбэк на дефолт
|
||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Ошибка при обработке логотипа:', error.message);
|
||||
// Используем fallback логотип
|
||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||
}
|
||||
|
||||
console.log('Данные для деплоя DLE:', deployData);
|
||||
|
||||
// Предварительная проверка балансов во всех сетях
|
||||
// Предварительная проверка балансов (через приватный ключ)
|
||||
deployProgress.value = 20;
|
||||
deployStatus.value = 'Проверка баланса во всех выбранных сетях...';
|
||||
try {
|
||||
const pre = await axios.post('/dle-v2/precheck', {
|
||||
const pre = await api.post('/dle-v2/precheck', {
|
||||
supportedChainIds: deployData.supportedChainIds,
|
||||
privateKey: deployData.privateKey
|
||||
privateKey: unifiedPrivateKey.value
|
||||
});
|
||||
const preData = pre.data?.data;
|
||||
if (pre.data?.success && preData) {
|
||||
const lacks = (preData.insufficient || []);
|
||||
const warnings = (preData.warnings || []);
|
||||
|
||||
if (lacks.length > 0) {
|
||||
const lines = (preData.balances || []).map(b => {
|
||||
const status = b.ok ? '✅' : '❌';
|
||||
const warning = warnings.includes(b.chainId) ? ' ⚠️' : '';
|
||||
return `${status} Chain ${b.chainId}: ${b.balanceEth} ETH (мин. ${b.minRequiredEth} ETH)${warning}`;
|
||||
});
|
||||
|
||||
const message = `Проверка балансов завершена:\n\n${lines.join('\n')}\n\n${lacks.length > 0 ? '❌ Недостаточно средств в некоторых сетях!' : ''}\n${warnings.length > 0 ? '⚠️ Предупреждения в некоторых сетях!' : ''}`;
|
||||
|
||||
if (lacks.length > 0) {
|
||||
alert(message);
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
} else if (warnings.length > 0) {
|
||||
const proceed = confirm(message + '\n\nПродолжить деплой?');
|
||||
if (!proceed) {
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const message = `❌ Недостаточно средств в некоторых сетях!`;
|
||||
alert(message);
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Проверка балансов пройдена:', preData.summary);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Ошибка проверки балансов:', e.message);
|
||||
// Если precheck недоступен, не блокируем — продолжаем
|
||||
}
|
||||
|
||||
deployProgress.value = 30;
|
||||
deployStatus.value = 'Компиляция смарт-контрактов...';
|
||||
|
||||
// Автокомпиляция контрактов перед деплоем
|
||||
console.log('🔨 Запуск автокомпиляции...');
|
||||
// Автокомпиляция контрактов
|
||||
try {
|
||||
const compileResponse = await axios.post('/compile-contracts');
|
||||
const compileResponse = await api.post('/compile-contracts');
|
||||
console.log('✅ Контракты скомпилированы:', compileResponse.data);
|
||||
} catch (compileError) {
|
||||
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
|
||||
// Продолжаем деплой даже если компиляция не удалась
|
||||
}
|
||||
|
||||
deployProgress.value = 40;
|
||||
deployStatus.value = 'Отправка данных на сервер...';
|
||||
deployStatus.value = 'Деплой DLE контракта...';
|
||||
|
||||
// Вызов API для деплоя
|
||||
deployProgress.value = 50;
|
||||
deployStatus.value = 'Деплой смарт-контракта в блокчейне...';
|
||||
|
||||
const response = await axios.post('/dle-v2', deployData);
|
||||
|
||||
// Деплой будет выполнен в DeploymentWizard
|
||||
// Здесь только показываем мастер деплоя
|
||||
deployProgress.value = 80;
|
||||
deployStatus.value = 'Проверка результатов деплоя...';
|
||||
deployStatus.value = 'Запуск мастера деплоя...';
|
||||
|
||||
if (response.data.success) {
|
||||
const result = response.data.data;
|
||||
|
||||
// Проверяем результаты мульти-чейн деплоя
|
||||
if (result.networks && Array.isArray(result.networks)) {
|
||||
const successfulNetworks = result.networks.filter(n => n.success);
|
||||
const failedNetworks = result.networks.filter(n => !n.success);
|
||||
|
||||
if (failedNetworks.length > 0) {
|
||||
console.warn('Некоторые сети не удалось развернуть:', failedNetworks);
|
||||
}
|
||||
|
||||
if (successfulNetworks.length > 0) {
|
||||
// Проверяем, что все адреса одинаковые
|
||||
const addresses = successfulNetworks.map(n => n.address);
|
||||
const uniqueAddresses = [...new Set(addresses)];
|
||||
|
||||
if (uniqueAddresses.length === 1) {
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = `✅ DLE успешно развернут в ${successfulNetworks.length} сетях с одинаковым адресом!`;
|
||||
|
||||
console.log('🎉 Мульти-чейн деплой завершен успешно!');
|
||||
console.log('Адрес DLE:', uniqueAddresses[0]);
|
||||
console.log('Сети:', successfulNetworks.map(n => `Chain ${n.chainId}: ${n.address}`));
|
||||
|
||||
// Небольшая задержка для показа успешного завершения
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
}, 3000);
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ ОШИБКА: Адреса DLE в разных сетях не совпадают! Это может указывать на проблему с CREATE2.');
|
||||
}
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Не удалось развернуть DLE ни в одной сети');
|
||||
}
|
||||
} else {
|
||||
// Fallback для одиночного деплоя
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = '✅ DLE успешно развернут!';
|
||||
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
router.push('/management');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое: ' + (response.data.message || response.data.error));
|
||||
}
|
||||
// Показываем мастер деплоя
|
||||
showDeploymentWizard.value = true;
|
||||
|
||||
// Мастер деплоя сам выполнит деплой
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Ошибка деплоя DLE:', error);
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое смарт-контракта: ' + error.message);
|
||||
console.error('Ошибка при запуске деплоя:', error);
|
||||
deployStatus.value = `❌ Ошибка: ${error.message}`;
|
||||
deployProgress.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик завершения поэтапного деплоя
|
||||
const handleDeploymentCompleted = (result) => {
|
||||
console.log('🎉 Поэтапный деплой завершен:', result);
|
||||
showDeploymentWizard.value = false;
|
||||
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
};
|
||||
|
||||
// Валидация формы
|
||||
const isFormValid = computed(() => {
|
||||
const isFormValid = computed(() => {
|
||||
const validation = {
|
||||
jurisdiction: !!dleSettings.jurisdiction,
|
||||
name: !!dleSettings.name,
|
||||
tokenSymbol: !!dleSettings.tokenSymbol,
|
||||
partners: dleSettings.partners.length > 0,
|
||||
partnersValid: dleSettings.partners.every(partner => partner.address && partner.amount > 0),
|
||||
quorum: dleSettings.governanceQuorum > 0 && dleSettings.governanceQuorum <= 100,
|
||||
networks: selectedNetworks.value.length > 0,
|
||||
privateKey: !!unifiedPrivateKey.value,
|
||||
keyValid: !!keyValidation.unified?.isValid,
|
||||
coordinates: validateCoordinates(dleSettings.coordinates)
|
||||
};
|
||||
|
||||
console.log('🔍 Валидация формы:', validation);
|
||||
console.log('🔍 selectedNetworks.value:', selectedNetworks.value);
|
||||
console.log('🔍 adminTokenCheck:', adminTokenCheck.value);
|
||||
console.log('🔍 showDeployProgress:', showDeployProgress.value);
|
||||
console.log('🔍 unifiedPrivateKey.value:', unifiedPrivateKey.value);
|
||||
console.log('🔍 keyValidation.unified:', keyValidation.unified);
|
||||
console.log('🔍 dleSettings.coordinates:', dleSettings.coordinates);
|
||||
console.log('🔍 Кнопка должна быть активна:', !(!validation.jurisdiction || !validation.name || !validation.tokenSymbol || !validation.partners || !validation.partnersValid || !validation.quorum || !validation.networks || !validation.privateKey || !validation.keyValid || !validation.coordinates) && adminTokenCheck.value.isAdmin && !adminTokenCheck.value.isLoading && !showDeployProgress.value);
|
||||
|
||||
return Boolean(
|
||||
dleSettings.jurisdiction &&
|
||||
dleSettings.name &&
|
||||
dleSettings.tokenSymbol &&
|
||||
(dleSettings.partners.length > 0) &&
|
||||
dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
|
||||
dleSettings.governanceQuorum > 0 &&
|
||||
dleSettings.governanceQuorum <= 100 &&
|
||||
(dleSettings.selectedNetworks.length > 0) &&
|
||||
// Проверка приватного ключа
|
||||
unifiedPrivateKey.value &&
|
||||
keyValidation.unified?.isValid &&
|
||||
// Валидация координат
|
||||
validateCoordinates(dleSettings.coordinates)
|
||||
validation.jurisdiction &&
|
||||
validation.name &&
|
||||
validation.tokenSymbol &&
|
||||
validation.partners &&
|
||||
validation.partnersValid &&
|
||||
validation.quorum &&
|
||||
validation.networks &&
|
||||
validation.privateKey &&
|
||||
validation.keyValid &&
|
||||
validation.coordinates
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2715,7 +2774,7 @@ async function submitDeploy() {
|
||||
if (logoFile.value) {
|
||||
const form = new FormData();
|
||||
form.append('logo', logoFile.value);
|
||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploadResp = await api.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||
if (uploaded) {
|
||||
deployData.logoURI = uploaded;
|
||||
@@ -4385,6 +4444,85 @@ async function submitDeploy() {
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Стили для информации о деплое */
|
||||
.deployment-info {
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.deployment-info h4 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.deployment-description {
|
||||
color: #6c757d;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.deployment-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.feature-item i {
|
||||
color: #28a745;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.feature-item span {
|
||||
color: #495057;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Стили для мастера деплоя */
|
||||
.deployment-wizard-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wizard-container {
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.deploy-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
>
|
||||
|
||||
<div class="proposal-header">
|
||||
<h5>{{ proposal.description || 'Без описания' }}</h5>
|
||||
<h5>{{ getProposalTitle(proposal) }}</h5>
|
||||
<span class="proposal-status" :class="proposal.status">
|
||||
{{ getProposalStatusText(proposal.status) }}
|
||||
</span>
|
||||
@@ -92,6 +92,27 @@
|
||||
<div class="detail-item">
|
||||
<strong>Дедлайн:</strong> {{ formatDate(proposal.deadline) }}
|
||||
</div>
|
||||
|
||||
<!-- Детальная информация о модуле -->
|
||||
<div v-if="proposal.decodedData" class="module-details">
|
||||
<div class="detail-item">
|
||||
<strong>Тип модуля:</strong> {{ getModuleName(proposal.decodedData.moduleId) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Адрес модуля:</strong>
|
||||
<a :href="getEtherscanUrl(proposal.decodedData.moduleAddress, proposal.decodedData.chainId)"
|
||||
target="_blank" class="address-link">
|
||||
{{ shortenAddress(proposal.decodedData.moduleAddress) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Сеть:</strong> {{ getChainName(proposal.decodedData.chainId) }}
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Длительность:</strong> {{ formatDuration(proposal.decodedData.duration) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-item">
|
||||
<strong>Голоса:</strong>
|
||||
<div class="votes-container">
|
||||
@@ -100,7 +121,7 @@
|
||||
<span class="against">Против: {{ formatVotes(proposal.againstVotes) }}</span>
|
||||
</div>
|
||||
<div class="quorum-info">
|
||||
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum() }}%</span>
|
||||
<span class="quorum-percentage">Кворум: {{ getQuorumPercentage(proposal) }}% из {{ getRequiredQuorum(proposal) }}%</span>
|
||||
</div>
|
||||
<div class="quorum-progress">
|
||||
<div class="progress-bar">
|
||||
@@ -140,13 +161,21 @@
|
||||
<i class="fas fa-times"></i> Против
|
||||
</button>
|
||||
<button
|
||||
v-if="canExecute(proposal) && props.isAuthenticated && hasAdminRights()"
|
||||
v-if="canExecute(proposal) && props.isAuthenticated"
|
||||
class="btn btn-sm btn-primary"
|
||||
@click="executeProposalLocal(proposal.id)"
|
||||
>
|
||||
<i class="fas fa-play"></i> Исполнить
|
||||
</button>
|
||||
|
||||
<!-- Информация для не-инициаторов -->
|
||||
<div v-else-if="proposal.state === 5 && !proposal.executed && props.isAuthenticated" class="execution-notice">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Только инициатор предложения может его исполнить
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Информация для неавторизованных пользователей -->
|
||||
<div v-if="!props.isAuthenticated" class="auth-notice">
|
||||
<small class="text-muted">
|
||||
@@ -533,7 +562,7 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { useAuthContext } from '../../composables/useAuth';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import { getDLEInfo, getSupportedChains } from '../../services/dleV2Service.js';
|
||||
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI } from '../../services/proposalsService.js';
|
||||
import { getProposals, createProposal as createProposalAPI, voteOnProposal as voteForProposalAPI, executeProposal as executeProposalAPI, decodeProposalData } from '../../services/proposalsService.js';
|
||||
import api from '../../api/axios';
|
||||
const showTargetChains = computed(() => {
|
||||
// Для offchain-действий не требуется ончейн исполнение (здесь типы пока ончейн)
|
||||
@@ -543,6 +572,306 @@ const showTargetChains = computed(() => {
|
||||
import wsClient from '../../utils/websocket.js';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
// Best Practice: WebSocket-based подписка на обновления голосования
|
||||
function subscribeToVoteUpdates(txHash, proposalId, actionType) {
|
||||
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для:', { txHash, proposalId, actionType });
|
||||
|
||||
// Создаем уникальный обработчик для этой транзакции
|
||||
const voteHandler = (data) => {
|
||||
console.log('[DleProposalsView] Получено WebSocket уведомление о голосовании:', data);
|
||||
|
||||
// Проверяем, что это наша транзакция
|
||||
if (data.txHash === txHash || data.proposalId === proposalId) {
|
||||
console.log('[DleProposalsView] Найдено совпадение транзакции, обновляем данные');
|
||||
|
||||
// Обновляем данные
|
||||
loadDleData().then(() => {
|
||||
// Показываем успешное уведомление
|
||||
showSuccessNotification(txHash, actionType);
|
||||
});
|
||||
|
||||
// Отписываемся от уведомлений
|
||||
wsClient.off('proposal_voted', voteHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// Подписываемся на уведомления о голосовании
|
||||
wsClient.on('proposal_voted', voteHandler);
|
||||
|
||||
// Устанавливаем таймаут на случай, если WebSocket не сработает
|
||||
setTimeout(() => {
|
||||
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений, отписываемся');
|
||||
wsClient.off('proposal_voted', voteHandler);
|
||||
|
||||
// Fallback: обновляем данные в любом случае
|
||||
loadDleData().then(() => {
|
||||
showTimeoutNotification(txHash, actionType);
|
||||
});
|
||||
}, 60000); // 60 секунд таймаут
|
||||
}
|
||||
|
||||
// WebSocket-based подписка на обновления исполнения
|
||||
function subscribeToExecutionUpdates(txHash, proposalId) {
|
||||
console.log('[DleProposalsView] Подписываемся на WebSocket уведомления для исполнения:', { txHash, proposalId });
|
||||
|
||||
// Создаем уникальный обработчик для этой транзакции
|
||||
const executionHandler = (data) => {
|
||||
console.log('[DleProposalsView] Получено WebSocket уведомление об исполнении:', data);
|
||||
|
||||
// Проверяем, что это наша транзакция
|
||||
if (data.txHash === txHash || data.proposalId === proposalId) {
|
||||
console.log('[DleProposalsView] Найдено совпадение транзакции исполнения, обновляем данные');
|
||||
|
||||
// Обновляем данные
|
||||
loadDleData().then(() => {
|
||||
// Показываем успешное уведомление
|
||||
showSuccessNotification(txHash, 'execution');
|
||||
});
|
||||
|
||||
// Отписываемся от уведомлений
|
||||
wsClient.off('proposal_executed', executionHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// Подписываемся на уведомления об исполнении
|
||||
wsClient.on('proposal_executed', executionHandler);
|
||||
|
||||
// Устанавливаем таймаут на случай, если WebSocket не сработает
|
||||
setTimeout(() => {
|
||||
console.warn('[DleProposalsView] Таймаут WebSocket уведомлений об исполнении, отписываемся');
|
||||
wsClient.off('proposal_executed', executionHandler);
|
||||
|
||||
// Fallback: обновляем данные в любом случае
|
||||
loadDleData().then(() => {
|
||||
showTimeoutNotification(txHash, 'execution');
|
||||
});
|
||||
}, 60000); // 60 секунд таймаут
|
||||
}
|
||||
|
||||
// Функция для отслеживания транзакции исполнения на backend
|
||||
async function trackExecutionTransaction(txHash, dleAddress, proposalId) {
|
||||
try {
|
||||
console.log('[DleProposalsView] Запускаем отслеживание транзакции исполнения на backend:', { txHash, dleAddress, proposalId });
|
||||
|
||||
const response = await api.post('/dle-proposals/track-execution-transaction', {
|
||||
txHash: txHash,
|
||||
dleAddress: dleAddress,
|
||||
proposalId: proposalId
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('[DleProposalsView] Backend подтвердил транзакцию исполнения:', response.data);
|
||||
} else {
|
||||
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию исполнения:', response.data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DleProposalsView] Ошибка при отслеживании транзакции исполнения на backend:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для отслеживания транзакции голосования на backend
|
||||
async function trackVoteTransaction(txHash, dleAddress, proposalId, support) {
|
||||
try {
|
||||
console.log('[DleProposalsView] Запускаем отслеживание транзакции на backend:', { txHash, dleAddress, proposalId, support });
|
||||
|
||||
const response = await api.post('/dle-proposals/track-vote-transaction', {
|
||||
txHash: txHash,
|
||||
dleAddress: dleAddress,
|
||||
proposalId: proposalId,
|
||||
support: support
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('[DleProposalsView] Backend подтвердил транзакцию:', response.data);
|
||||
} else {
|
||||
console.warn('[DleProposalsView] Backend не смог подтвердить транзакцию:', response.data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DleProposalsView] Ошибка при отслеживании транзакции на backend:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Показ уведомления о транзакции
|
||||
function showTransactionNotification(txHash, message) {
|
||||
// Создаем уведомление с ссылкой на Etherscan
|
||||
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||
|
||||
// Можно использовать toast-библиотеку или создать кастомное уведомление
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'transaction-notification';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-header">
|
||||
<span class="notification-icon">⏳</span>
|
||||
<span class="notification-title">${message}</span>
|
||||
</div>
|
||||
<div class="notification-body">
|
||||
<p>Ожидаем подтверждения транзакции...</p>
|
||||
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||
Посмотреть в Etherscan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Добавляем стили
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Автоматически удаляем через 10 секунд
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Показ успешного уведомления
|
||||
function showSuccessNotification(txHash, actionType) {
|
||||
const actionText = actionType === 'vote' ? 'Голосование подтверждено!' : 'Голосование "против" подтверждено!';
|
||||
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'success-notification';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-header">
|
||||
<span class="notification-icon">✅</span>
|
||||
<span class="notification-title">${actionText}</span>
|
||||
</div>
|
||||
<div class="notification-body">
|
||||
<p>Данные обновлены</p>
|
||||
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||
Посмотреть в Etherscan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Показ уведомления об ошибке
|
||||
function showErrorNotification(txHash, message) {
|
||||
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'error-notification';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-header">
|
||||
<span class="notification-icon">❌</span>
|
||||
<span class="notification-title">${message}</span>
|
||||
</div>
|
||||
<div class="notification-body">
|
||||
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||
Посмотреть в Etherscan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
// Показ уведомления о таймауте
|
||||
function showTimeoutNotification(txHash, actionType) {
|
||||
const actionText = actionType === 'vote' ? 'Голосование' : 'Голосование "против"';
|
||||
const etherscanUrl = `https://sepolia.etherscan.io/tx/${txHash}`;
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'timeout-notification';
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<div class="notification-header">
|
||||
<span class="notification-icon">⏰</span>
|
||||
<span class="notification-title">${actionText} отправлено</span>
|
||||
</div>
|
||||
<div class="notification-body">
|
||||
<p>Подтверждение не получено, но данные обновлены</p>
|
||||
<a href="${etherscanUrl}" target="_blank" class="etherscan-link">
|
||||
Посмотреть в Etherscan
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
dleAddress: { type: String, required: false, default: null },
|
||||
dleContract: { type: Object, required: false, default: null },
|
||||
@@ -670,15 +999,30 @@ async function loadDleData() {
|
||||
console.log('[Frontend] Массив предложений:', proposalsData);
|
||||
|
||||
// Преобразуем данные из API в формат для frontend
|
||||
proposals.value = proposalsData.map(proposal => {
|
||||
proposals.value = await Promise.all(proposalsData.map(async (proposal) => {
|
||||
const transformedProposal = {
|
||||
...proposal,
|
||||
status: getProposalStatus(proposal),
|
||||
deadline: proposal.deadline || 0
|
||||
};
|
||||
|
||||
// Если есть transactionHash, декодируем данные предложения
|
||||
if (proposal.transactionHash) {
|
||||
try {
|
||||
console.log('[Frontend] Декодируем данные предложения:', proposal.transactionHash);
|
||||
const decodedData = await decodeProposalData(proposal.transactionHash);
|
||||
if (decodedData.success) {
|
||||
transformedProposal.decodedData = decodedData.data;
|
||||
console.log('[Frontend] Декодированные данные:', decodedData.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Frontend] Ошибка декодирования данных предложения:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Frontend] Преобразованное предложение:', transformedProposal);
|
||||
return transformedProposal;
|
||||
});
|
||||
}));
|
||||
|
||||
console.log('[Frontend] Итоговый список предложений:', proposals.value);
|
||||
|
||||
@@ -819,8 +1163,13 @@ function getProposalStatus(proposal) {
|
||||
const forVotes = Number(proposal.forVotes) || 0;
|
||||
const againstVotes = Number(proposal.againstVotes) || 0;
|
||||
|
||||
// Если есть голоса, определяем результат
|
||||
if (forVotes > 0 || againstVotes > 0) {
|
||||
// Проверяем, достигнут ли кворум
|
||||
const quorumPercentage = getQuorumPercentage(proposal);
|
||||
const requiredQuorum = getRequiredQuorum(proposal);
|
||||
const quorumReached = quorumPercentage >= requiredQuorum;
|
||||
|
||||
// Если есть голоса И кворум достигнут, определяем результат
|
||||
if ((forVotes > 0 || againstVotes > 0) && quorumReached) {
|
||||
if (forVotes > againstVotes) {
|
||||
return 'succeeded';
|
||||
} else if (againstVotes > forVotes) {
|
||||
@@ -828,6 +1177,7 @@ function getProposalStatus(proposal) {
|
||||
}
|
||||
}
|
||||
|
||||
// Если кворум не достигнут или нет голосов, предложение активно
|
||||
return 'active';
|
||||
}
|
||||
|
||||
@@ -843,6 +1193,61 @@ function getProposalStatusText(status) {
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
function getProposalTitle(proposal) {
|
||||
// Если есть декодированные данные, показываем детальную информацию
|
||||
if (proposal.decodedData) {
|
||||
const { moduleId, moduleAddress, chainId, duration } = proposal.decodedData;
|
||||
|
||||
// Декодируем moduleId из hex в строку
|
||||
let moduleName = 'Неизвестный модуль';
|
||||
try {
|
||||
moduleName = ethers.toUtf8String(moduleId).replace(/\0/g, '');
|
||||
} catch (e) {
|
||||
console.log('Не удалось декодировать moduleId:', moduleId);
|
||||
}
|
||||
|
||||
return `Добавить модуль: ${moduleName}`;
|
||||
}
|
||||
|
||||
// Иначе показываем обычное описание
|
||||
return proposal.description || 'Без описания';
|
||||
}
|
||||
|
||||
function getModuleName(moduleId) {
|
||||
try {
|
||||
return ethers.toUtf8String(moduleId).replace(/\0/g, '');
|
||||
} catch (e) {
|
||||
return 'Неизвестный модуль';
|
||||
}
|
||||
}
|
||||
|
||||
function getEtherscanUrl(address, chainId) {
|
||||
const chainMap = {
|
||||
1: 'https://etherscan.io',
|
||||
11155111: 'https://sepolia.etherscan.io',
|
||||
17000: 'https://holesky.etherscan.io',
|
||||
421614: 'https://sepolia.arbiscan.io',
|
||||
84532: 'https://sepolia.basescan.org'
|
||||
};
|
||||
|
||||
const baseUrl = chainMap[chainId] || 'https://etherscan.io';
|
||||
return `${baseUrl}/address/${address}`;
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days} дн. ${hours} ч.`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours} ч. ${minutes} мин.`;
|
||||
} else {
|
||||
return `${minutes} мин.`;
|
||||
}
|
||||
}
|
||||
|
||||
function getProposalStatusClass(status) {
|
||||
const classMap = {
|
||||
'pending': 'status-pending',
|
||||
@@ -901,16 +1306,25 @@ function getQuorumPercentage(proposal) {
|
||||
|
||||
function getQuorumProgress(proposal) {
|
||||
const percentage = getQuorumPercentage(proposal);
|
||||
const requiredQuorum = getRequiredQuorum();
|
||||
const requiredQuorum = getRequiredQuorum(proposal);
|
||||
const progress = Math.min((percentage / requiredQuorum) * 100, 100);
|
||||
console.log('[Quorum] Прогресс кворума:', { percentage, requiredQuorum, progress });
|
||||
return progress;
|
||||
}
|
||||
|
||||
function getRequiredQuorum() {
|
||||
function getRequiredQuorum(proposal = null) {
|
||||
// Если есть данные о предложении с quorumRequired, используем их
|
||||
if (proposal && proposal.quorumRequired && selectedDle.value?.totalSupply) {
|
||||
const totalSupplyWei = parseFloat(selectedDle.value.totalSupply) * Math.pow(10, 18);
|
||||
const quorumPercentage = (proposal.quorumRequired / totalSupplyWei) * 100;
|
||||
console.log('[Quorum] Требуемый кворум из предложения:', quorumPercentage, 'quorumRequired:', proposal.quorumRequired, 'totalSupply:', totalSupplyWei);
|
||||
return Math.round(quorumPercentage * 100) / 100;
|
||||
}
|
||||
|
||||
// Fallback к данным DLE
|
||||
const quorum = selectedDle.value?.quorumPercentage || 51;
|
||||
console.log('[Quorum] Требуемый кворум из DLE:', quorum, 'DLE данные:', selectedDle.value);
|
||||
return quorum; // По умолчанию 51% если данные не загружены
|
||||
return quorum;
|
||||
}
|
||||
|
||||
function formatVotes(votes) {
|
||||
@@ -974,15 +1388,17 @@ function canExecute(proposal) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const deadline = proposal.deadline || 0;
|
||||
|
||||
// Предложение можно выполнить только если:
|
||||
// 1. Дедлайн истек
|
||||
// 2. Кворум достигнут
|
||||
// 3. Предложение еще не выполнено
|
||||
// Предложение можно выполнить если:
|
||||
// 1. Кворум достигнут ИЛИ предложение уже принято (state: 5)
|
||||
// 2. Предложение еще не выполнено
|
||||
const quorumPercentage = getQuorumPercentage(proposal);
|
||||
const requiredQuorum = getRequiredQuorum();
|
||||
const requiredQuorum = getRequiredQuorum(proposal);
|
||||
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
|
||||
const deadlinePassed = deadline > 0 && now >= deadline;
|
||||
|
||||
// Если предложение уже принято (state: 5), можно исполнять
|
||||
const isProposalPassed = proposal.state === 5 || proposal.isPassed === true;
|
||||
|
||||
// Добавляем отладочную информацию
|
||||
console.log('[canExecute] Проверка предложения:', {
|
||||
proposalId: proposal.id,
|
||||
@@ -992,10 +1408,29 @@ function canExecute(proposal) {
|
||||
deadline,
|
||||
now,
|
||||
deadlinePassed,
|
||||
executed: proposal.executed
|
||||
executed: proposal.executed,
|
||||
state: proposal.state,
|
||||
isPassed: proposal.isPassed,
|
||||
isProposalPassed
|
||||
});
|
||||
|
||||
return deadlinePassed && hasReachedQuorum && !proposal.executed;
|
||||
// Проверяем, что текущий пользователь - инициатор предложения
|
||||
const isInitiator = address.value && proposal.initiator &&
|
||||
address.value.toLowerCase() === proposal.initiator.toLowerCase();
|
||||
|
||||
console.log('[canExecute] Проверка инициатора:', {
|
||||
currentAddress: address.value,
|
||||
proposalInitiator: proposal.initiator,
|
||||
isInitiator
|
||||
});
|
||||
|
||||
// Можно исполнять если:
|
||||
// 1. (кворум достигнут И дедлайн истек) ИЛИ предложение уже принято
|
||||
// 2. Пользователь - инициатор предложения
|
||||
// 3. Предложение не выполнено
|
||||
return ((hasReachedQuorum && deadlinePassed) || isProposalPassed) &&
|
||||
isInitiator &&
|
||||
!proposal.executed;
|
||||
}
|
||||
|
||||
function hasSigned(proposalId) {
|
||||
@@ -1191,10 +1626,107 @@ async function signProposalLocal(proposalId) {
|
||||
console.log('[Debug] Попытка подписи для предложения:', proposalId);
|
||||
console.log('[Debug] Адрес кошелька:', address.value);
|
||||
|
||||
await voteForProposalAPI(dleAddress.value, proposalId, true); // Подпись = голос "за"
|
||||
// Получаем данные транзакции от backend
|
||||
const result = await voteForProposalAPI(dleAddress.value, proposalId, true);
|
||||
|
||||
await loadDleData();
|
||||
alert('✅ Предложение подписано!');
|
||||
if (result.success) {
|
||||
console.log('[DleProposalsView] Данные транзакции голосования получены:', result);
|
||||
|
||||
// Отправляем транзакцию через MetaMask
|
||||
try {
|
||||
// Проверяем валидность адреса
|
||||
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||
}
|
||||
|
||||
// Проверяем, что есть подключенный аккаунт
|
||||
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
|
||||
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
}
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
|
||||
}
|
||||
|
||||
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
|
||||
|
||||
// Проверяем подключение к правильной сети
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
const expectedChainId = '0xaa36a7'; // Sepolia
|
||||
|
||||
if (chainId !== expectedChainId) {
|
||||
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
|
||||
|
||||
try {
|
||||
// Пытаемся переключиться на Sepolia
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: expectedChainId }],
|
||||
});
|
||||
console.log('[DleProposalsView] Успешно переключились на Sepolia');
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log('[DleProposalsView] Добавляем Sepolia сеть');
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: expectedChainId,
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'SepoliaETH',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DleProposalsView] Отправляем транзакцию голосования:', {
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
});
|
||||
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [{
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
}]
|
||||
});
|
||||
|
||||
console.log('[DleProposalsView] Транзакция голосования отправлена:', txHash);
|
||||
|
||||
// Показываем уведомление с возможностью отслеживания
|
||||
showTransactionNotification(txHash, 'Голосование отправлено!');
|
||||
|
||||
// Подписываемся на WebSocket уведомления о голосовании
|
||||
subscribeToVoteUpdates(txHash, proposalId, 'vote');
|
||||
|
||||
// Запускаем отслеживание транзакции на backend
|
||||
trackVoteTransaction(txHash, dleAddress.value, proposalId, true);
|
||||
|
||||
} catch (txError) {
|
||||
console.error('[DleProposalsView] Ошибка отправки транзакции голосования:', txError);
|
||||
alert('❌ Ошибка отправки транзакции голосования: ' + txError.message);
|
||||
}
|
||||
} else {
|
||||
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при подписании:', error);
|
||||
@@ -1225,10 +1757,107 @@ async function cancelSignatureLocal(proposalId) {
|
||||
console.log('[Debug] Попытка голосования "против" для предложения:', proposalId);
|
||||
console.log('[Debug] Адрес кошелька:', address.value);
|
||||
|
||||
await voteForProposalAPI(dleAddress.value, proposalId, false); // Голос "против"
|
||||
// Получаем данные транзакции от backend
|
||||
const result = await voteForProposalAPI(dleAddress.value, proposalId, false);
|
||||
|
||||
await loadDleData();
|
||||
alert('✅ Ваш голос "против" учтен!');
|
||||
if (result.success) {
|
||||
console.log('[DleProposalsView] Данные транзакции голосования "против" получены:', result);
|
||||
|
||||
// Отправляем транзакцию через MetaMask
|
||||
try {
|
||||
// Проверяем валидность адреса
|
||||
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||
}
|
||||
|
||||
// Проверяем, что есть подключенный аккаунт
|
||||
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
|
||||
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
}
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
|
||||
}
|
||||
|
||||
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
|
||||
|
||||
// Проверяем подключение к правильной сети
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
const expectedChainId = '0xaa36a7'; // Sepolia
|
||||
|
||||
if (chainId !== expectedChainId) {
|
||||
console.log(`[DleProposalsView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
|
||||
|
||||
try {
|
||||
// Пытаемся переключиться на Sepolia
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: expectedChainId }],
|
||||
});
|
||||
console.log('[DleProposalsView] Успешно переключились на Sepolia');
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log('[DleProposalsView] Добавляем Sepolia сеть');
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: expectedChainId,
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'SepoliaETH',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DleProposalsView] Отправляем транзакцию голосования "против":', {
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
});
|
||||
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [{
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
}]
|
||||
});
|
||||
|
||||
console.log('[DleProposalsView] Транзакция голосования "против" отправлена:', txHash);
|
||||
|
||||
// Показываем уведомление с возможностью отслеживания
|
||||
showTransactionNotification(txHash, 'Голосование "против" отправлено!');
|
||||
|
||||
// Подписываемся на WebSocket уведомления о голосовании
|
||||
subscribeToVoteUpdates(txHash, proposalId, 'vote-against');
|
||||
|
||||
// Запускаем отслеживание транзакции на backend
|
||||
trackVoteTransaction(txHash, dleAddress.value, proposalId, false);
|
||||
|
||||
} catch (txError) {
|
||||
console.error('[DleProposalsView] Ошибка отправки транзакции голосования "против":', txError);
|
||||
alert('❌ Ошибка отправки транзакции голосования "против": ' + txError.message);
|
||||
}
|
||||
} else {
|
||||
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при голосовании "против":', error);
|
||||
@@ -1243,23 +1872,131 @@ async function cancelSignatureLocal(proposalId) {
|
||||
|
||||
// Исполнение предложения
|
||||
async function executeProposalLocal(proposalId) {
|
||||
// Проверка прав админа для исполнения
|
||||
// Проверка авторизации
|
||||
if (!props.isAuthenticated) {
|
||||
alert('❌ Для исполнения предложений необходимо авторизоваться в приложении');
|
||||
return;
|
||||
}
|
||||
|
||||
// Дополнительная проверка на права админа
|
||||
if (!hasAdminRights()) {
|
||||
alert('❌ Для исполнения предложений необходимы права администратора');
|
||||
// Проверка, что пользователь - инициатор предложения
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (!proposal) {
|
||||
alert('❌ Предложение не найдено');
|
||||
return;
|
||||
}
|
||||
|
||||
const isInitiator = address.value && proposal.initiator &&
|
||||
address.value.toLowerCase() === proposal.initiator.toLowerCase();
|
||||
|
||||
if (!isInitiator) {
|
||||
alert('❌ Только инициатор предложения может его исполнить');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await executeProposalAPI(dleAddress.value, proposalId);
|
||||
console.log('[Debug] Попытка исполнения предложения:', proposalId);
|
||||
console.log('[Debug] Адрес кошелька:', address.value);
|
||||
|
||||
await loadDleData();
|
||||
alert('✅ Предложение успешно исполнено!');
|
||||
// Получаем данные транзакции от backend
|
||||
const result = await executeProposalAPI(dleAddress.value, proposalId);
|
||||
|
||||
if (result.success) {
|
||||
console.log('[DleProposalsView] Данные транзакции исполнения получены:', result);
|
||||
|
||||
// Отправляем транзакцию через MetaMask
|
||||
try {
|
||||
// Проверяем валидность адреса
|
||||
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||
}
|
||||
|
||||
// Проверяем подключение к MetaMask
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не установлен');
|
||||
}
|
||||
|
||||
// Запрашиваем разрешение на подключение к MetaMask
|
||||
console.log('[DleProposalsView] Запрашиваем разрешение на подключение к MetaMask');
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
|
||||
if (accounts.length === 0) {
|
||||
throw new Error('Нет подключенных аккаунтов в MetaMask');
|
||||
}
|
||||
|
||||
console.log('[DleProposalsView] Подключенный аккаунт:', accounts[0]);
|
||||
|
||||
// Проверяем сеть
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
const expectedChainId = '0xaa36a7'; // Sepolia (11155111)
|
||||
|
||||
if (chainId !== expectedChainId) {
|
||||
console.log(`[DleProposalsView] Неправильная сеть! Текущая: ${chainId}, ожидается: ${expectedChainId}`);
|
||||
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: expectedChainId }],
|
||||
});
|
||||
} catch (switchError) {
|
||||
if (switchError.code === 4902) {
|
||||
console.log('[DleProposalsView] Добавляем Sepolia сеть');
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: expectedChainId,
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'SepoliaETH',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DleProposalsView] Отправляем транзакцию исполнения:', {
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
});
|
||||
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [{
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
}]
|
||||
});
|
||||
|
||||
console.log('[DleProposalsView] Транзакция исполнения отправлена:', txHash);
|
||||
|
||||
// Показываем уведомление с возможностью отслеживания
|
||||
showTransactionNotification(txHash, 'Исполнение предложения отправлено!');
|
||||
|
||||
// Подписываемся на WebSocket уведомления о исполнении
|
||||
subscribeToExecutionUpdates(txHash, proposalId);
|
||||
|
||||
// Запускаем отслеживание транзакции на backend
|
||||
trackExecutionTransaction(txHash, dleAddress.value, proposalId);
|
||||
|
||||
} catch (txError) {
|
||||
console.error('[DleProposalsView] Ошибка отправки транзакции исполнения:', txError);
|
||||
alert('❌ Ошибка отправки транзакции исполнения: ' + txError.message);
|
||||
}
|
||||
} else {
|
||||
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка при исполнении предложения:', error);
|
||||
@@ -1764,6 +2501,14 @@ onUnmounted(() => {
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.execution-notice {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #e2e3e5;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #6c757d;
|
||||
}
|
||||
|
||||
.proposal-status.canceled {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
@@ -1778,6 +2523,28 @@ onUnmounted(() => {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.module-details {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.module-details .detail-item {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.address-link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.address-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.votes-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -103,6 +103,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DLEReader -->
|
||||
<div class="module-deploy-card">
|
||||
<div class="module-content">
|
||||
<h4>DLEReader</h4>
|
||||
<p>Чтение данных DLE - API для получения информации о контракте и предложениях</p>
|
||||
<div class="module-features">
|
||||
<span class="feature-tag">API</span>
|
||||
<span class="feature-tag">Чтение</span>
|
||||
<span class="feature-tag">Данные</span>
|
||||
<span class="feature-tag">Интеграция</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-actions">
|
||||
<button
|
||||
class="btn btn-primary btn-deploy"
|
||||
@click="router.push(`/management/modules/deploy/reader?address=${route.query.address}`)"
|
||||
>
|
||||
<i class="fas fa-rocket"></i>
|
||||
Деплой
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CommunicationModule -->
|
||||
<div class="module-deploy-card">
|
||||
<div class="module-content">
|
||||
@@ -465,12 +488,44 @@
|
||||
<div class="modules-list">
|
||||
<div class="list-header">
|
||||
<h3>📋 Модули DLE</h3>
|
||||
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules">
|
||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules }"></i> Обновить
|
||||
<button class="btn btn-sm btn-outline-secondary" @click="loadModules" :disabled="isLoadingModules || isLoadingDeploymentStatus">
|
||||
<i class="fas fa-sync-alt" :class="{ 'fa-spin': isLoadingModules || isLoadingDeploymentStatus }"></i> Обновить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoadingModules" class="loading-modules">
|
||||
<!-- Статус деплоя -->
|
||||
<div v-if="isLoadingDeploymentStatus" class="deployment-status">
|
||||
<div class="status-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<span>Проверка статуса деплоя...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!canShowModules" class="deployment-status">
|
||||
<div class="status-message" :class="deploymentStatus">
|
||||
<div class="status-icon">
|
||||
<i v-if="deploymentStatus === 'completed'" class="fas fa-check-circle"></i>
|
||||
<i v-else-if="deploymentStatus === 'in_progress'" class="fas fa-spinner fa-spin"></i>
|
||||
<i v-else-if="deploymentStatus === 'failed'" class="fas fa-exclamation-triangle"></i>
|
||||
<i v-else-if="deploymentStatus === 'not_started'" class="fas fa-play-circle"></i>
|
||||
<i v-else class="fas fa-question-circle"></i>
|
||||
</div>
|
||||
<div class="status-content">
|
||||
<h4>{{ deploymentStatusMessage }}</h4>
|
||||
<p v-if="deploymentStatus === 'not_started'">
|
||||
Для активации модулей необходимо запустить поэтапный деплой DLE.
|
||||
</p>
|
||||
<p v-else-if="deploymentStatus === 'failed'">
|
||||
Проверьте логи деплоя и повторите попытку через форму деплоя.
|
||||
</p>
|
||||
<p v-else-if="deploymentStatus === 'in_progress'">
|
||||
Дождитесь завершения деплоя. Модули станут доступны автоматически.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isLoadingModules" class="loading-modules">
|
||||
<p>Загрузка модулей...</p>
|
||||
</div>
|
||||
|
||||
@@ -479,7 +534,7 @@
|
||||
<p>Используйте форму выше для добавления первого модуля</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="modules-grid">
|
||||
<div v-else-if="canShowModules && modules.length > 0" class="modules-grid">
|
||||
<div
|
||||
v-for="module in modules"
|
||||
:key="module.moduleId"
|
||||
@@ -590,7 +645,9 @@ import {
|
||||
createRemoveModuleProposal,
|
||||
isModuleActive,
|
||||
getModuleAddress,
|
||||
getAllModules
|
||||
getAllModules,
|
||||
getNetworksInfo,
|
||||
getDeploymentStatus
|
||||
} from '../../services/modulesService.js';
|
||||
import api from '../../api/axios';
|
||||
|
||||
@@ -612,11 +669,16 @@ const route = useRoute();
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
const modules = ref([]);
|
||||
const supportedNetworks = ref([]);
|
||||
const isLoadingModules = ref(false);
|
||||
const isCreating = ref(false);
|
||||
const isRemoving = ref(null);
|
||||
const isActivating = ref(null);
|
||||
const isVerifying = ref(null);
|
||||
|
||||
// Состояние деплоя
|
||||
const deploymentStatus = ref('unknown'); // 'unknown', 'completed', 'in_progress', 'failed', 'not_started'
|
||||
const isLoadingDeploymentStatus = ref(false);
|
||||
const lastUpdateTime = ref('');
|
||||
|
||||
// Форма нового модуля
|
||||
@@ -641,6 +703,23 @@ const modulesCount = computed(() => modules.value.length);
|
||||
const activeModulesCount = computed(() => modules.value.filter(m => m.isActive).length);
|
||||
const inactiveModulesCount = computed(() => modules.value.filter(m => !m.isActive).length);
|
||||
|
||||
// Статус деплоя
|
||||
const canShowModules = computed(() => deploymentStatus.value === 'completed');
|
||||
const deploymentStatusMessage = computed(() => {
|
||||
switch (deploymentStatus.value) {
|
||||
case 'completed':
|
||||
return 'Деплой завершен. Модули готовы к использованию.';
|
||||
case 'in_progress':
|
||||
return 'Деплой в процессе. Модули будут доступны после завершения.';
|
||||
case 'failed':
|
||||
return 'Деплой не удался. Проверьте логи и повторите попытку.';
|
||||
case 'not_started':
|
||||
return 'Деплой не начат. Запустите деплой для активации модулей.';
|
||||
default:
|
||||
return 'Статус деплоя неизвестен. Проверьте состояние системы.';
|
||||
}
|
||||
});
|
||||
|
||||
// Загрузка данных DLE
|
||||
async function loadDleData() {
|
||||
try {
|
||||
@@ -672,6 +751,37 @@ async function loadDleData() {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка статуса деплоя
|
||||
async function checkDeploymentStatus() {
|
||||
try {
|
||||
isLoadingDeploymentStatus.value = true;
|
||||
const dleAddress = route.query.address;
|
||||
|
||||
if (!dleAddress) {
|
||||
console.warn('[ModulesView] Адрес DLE не найден для проверки статуса деплоя');
|
||||
deploymentStatus.value = 'unknown';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Проверка статуса деплоя для DLE:', dleAddress);
|
||||
|
||||
const statusResponse = await getDeploymentStatus(dleAddress);
|
||||
console.log('[ModulesView] Статус деплоя:', statusResponse);
|
||||
|
||||
if (statusResponse.success) {
|
||||
deploymentStatus.value = statusResponse.data.status || 'unknown';
|
||||
} else {
|
||||
deploymentStatus.value = 'unknown';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка при проверке статуса деплоя:', error);
|
||||
deploymentStatus.value = 'unknown';
|
||||
} finally {
|
||||
isLoadingDeploymentStatus.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка модулей
|
||||
async function loadModules() {
|
||||
try {
|
||||
@@ -681,15 +791,30 @@ async function loadModules() {
|
||||
if (!dleAddress) {
|
||||
console.error('[ModulesView] Адрес DLE не указан');
|
||||
modules.value = [];
|
||||
supportedNetworks.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Загрузка модулей для DLE:', dleAddress);
|
||||
|
||||
// Загружаем модули через modulesService
|
||||
const modulesResponse = await getAllModules(dleAddress);
|
||||
// Сначала проверяем статус деплоя
|
||||
await checkDeploymentStatus();
|
||||
|
||||
console.log('[ModulesView] Ответ от API:', modulesResponse);
|
||||
// Если деплой не завершен, не загружаем модули
|
||||
if (deploymentStatus.value !== 'completed') {
|
||||
console.log('[ModulesView] Деплой не завершен, модули не загружаются. Статус:', deploymentStatus.value);
|
||||
modules.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Загружаем модули и информацию о сетях параллельно
|
||||
const [modulesResponse, networksResponse] = await Promise.all([
|
||||
getAllModules(dleAddress),
|
||||
getNetworksInfo(dleAddress)
|
||||
]);
|
||||
|
||||
console.log('[ModulesView] Ответ от API модулей:', modulesResponse);
|
||||
console.log('[ModulesView] Ответ от API сетей:', networksResponse);
|
||||
|
||||
if (modulesResponse.success) {
|
||||
modules.value = modulesResponse.data.modules || [];
|
||||
@@ -697,7 +822,7 @@ async function loadModules() {
|
||||
count: modules.value.length,
|
||||
modules: modules.value.map(m => ({
|
||||
name: m.moduleName,
|
||||
address: m.moduleAddress,
|
||||
addresses: m.addresses?.length || 0,
|
||||
active: m.isActive,
|
||||
id: m.moduleId
|
||||
})),
|
||||
@@ -717,6 +842,20 @@ async function loadModules() {
|
||||
console.error('[ModulesView] Ошибка загрузки модулей:', modulesResponse.error);
|
||||
modules.value = [];
|
||||
}
|
||||
|
||||
if (networksResponse.success) {
|
||||
supportedNetworks.value = networksResponse.data.networks || [];
|
||||
console.log('[ModulesView] Сети загружены успешно:', {
|
||||
count: supportedNetworks.value.length,
|
||||
networks: supportedNetworks.value.map(n => ({
|
||||
name: n.networkName,
|
||||
chainId: n.chainId
|
||||
}))
|
||||
});
|
||||
} else {
|
||||
console.error('[ModulesView] Ошибка загрузки сетей:', networksResponse.error);
|
||||
supportedNetworks.value = [];
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModulesView] Ошибка загрузки модулей:', error);
|
||||
@@ -726,6 +865,7 @@ async function loadModules() {
|
||||
status: error.response?.status
|
||||
});
|
||||
modules.value = [];
|
||||
supportedNetworks.value = [];
|
||||
} finally {
|
||||
isLoadingModules.value = false;
|
||||
}
|
||||
@@ -754,22 +894,112 @@ async function handleCreateAddModuleProposal() {
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('[ModulesView] Предложение создано:', result);
|
||||
alert('✅ Предложение для добавления модуля создано!');
|
||||
console.log('[ModulesView] Данные транзакции получены:', result);
|
||||
|
||||
// Очищаем форму
|
||||
newModule.value = {
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
description: '',
|
||||
duration: 86400,
|
||||
chainId: 11155111
|
||||
};
|
||||
|
||||
// Перезагружаем модули
|
||||
await loadModules();
|
||||
// Отправляем транзакцию через MetaMask
|
||||
try {
|
||||
// Проверяем валидность адреса
|
||||
if (!result.data.to || !result.data.to.startsWith('0x') || result.data.to.length !== 42) {
|
||||
throw new Error(`Неверный адрес контракта: ${result.data.to}`);
|
||||
}
|
||||
|
||||
// Проверяем, что адрес в правильном формате (checksum)
|
||||
const isValidAddress = /^0x[a-fA-F0-9]{40}$/.test(result.data.to);
|
||||
if (!isValidAddress) {
|
||||
throw new Error(`Адрес не в правильном формате: ${result.data.to}`);
|
||||
}
|
||||
|
||||
// Проверяем, что есть подключенный аккаунт
|
||||
let accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||
if (!accounts || accounts.length === 0) {
|
||||
console.log('[ModulesView] Запрашиваем разрешение на подключение к MetaMask');
|
||||
accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
}
|
||||
|
||||
if (!accounts || accounts.length === 0) {
|
||||
throw new Error('Не удалось получить доступ к аккаунтам MetaMask');
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Подключенный аккаунт:', accounts[0]);
|
||||
|
||||
// Проверяем подключение к правильной сети
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
const expectedChainId = '0x' + newModule.value.chainId.toString(16);
|
||||
|
||||
if (chainId !== expectedChainId) {
|
||||
console.log(`[ModulesView] Переключаемся с сети ${chainId} на ${expectedChainId}`);
|
||||
|
||||
try {
|
||||
// Пытаемся переключиться на Sepolia
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: expectedChainId }],
|
||||
});
|
||||
console.log('[ModulesView] Успешно переключились на Sepolia');
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log('[ModulesView] Добавляем Sepolia сеть');
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: expectedChainId,
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'SepoliaETH',
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
rpcUrls: ['https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Не удалось переключиться на Sepolia: ${switchError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[ModulesView] Отправляем транзакцию:', {
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
});
|
||||
|
||||
const txHash = await window.ethereum.request({
|
||||
method: 'eth_sendTransaction',
|
||||
params: [{
|
||||
from: accounts[0],
|
||||
to: result.data.to,
|
||||
data: result.data.data,
|
||||
value: result.data.value,
|
||||
gas: result.data.gasLimit
|
||||
}]
|
||||
});
|
||||
|
||||
console.log('[ModulesView] Транзакция отправлена:', txHash);
|
||||
alert(`✅ Транзакция отправлена! Hash: ${txHash}`);
|
||||
|
||||
// Очищаем форму
|
||||
newModule.value = {
|
||||
moduleId: '',
|
||||
moduleAddress: '',
|
||||
description: '',
|
||||
duration: 86400,
|
||||
chainId: 11155111
|
||||
};
|
||||
|
||||
// Перезагружаем модули
|
||||
await loadModules();
|
||||
|
||||
} catch (txError) {
|
||||
console.error('[ModulesView] Ошибка отправки транзакции:', txError);
|
||||
alert('❌ Ошибка отправки транзакции: ' + txError.message);
|
||||
}
|
||||
} else {
|
||||
alert('❌ Ошибка создания предложения: ' + result.error);
|
||||
alert('❌ Ошибка получения данных транзакции: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -854,7 +1084,8 @@ async function verifyModule(module, addressInfo) {
|
||||
dleAddress: dleAddress,
|
||||
moduleId: module.moduleId,
|
||||
moduleAddress: addressInfo.address,
|
||||
moduleName: module.moduleName
|
||||
moduleName: module.moduleName,
|
||||
chainId: addressInfo.chainId
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
@@ -898,16 +1129,12 @@ function getVerificationButtonTitle(verificationStatus) {
|
||||
|
||||
// Утилиты
|
||||
function getEtherscanUrl(address, networkIndex, chainId) {
|
||||
// Если есть chainId, используем его для определения правильного URL
|
||||
if (chainId) {
|
||||
const networkUrls = {
|
||||
11155111: `https://sepolia.etherscan.io/address/${address}`, // Sepolia
|
||||
17000: `https://holesky.etherscan.io/address/${address}`, // Holesky
|
||||
421614: `https://sepolia.arbiscan.io/address/${address}`, // Arbitrum Sepolia
|
||||
84532: `https://sepolia.basescan.org/address/${address}` // Base Sepolia
|
||||
};
|
||||
|
||||
return networkUrls[chainId] || `https://etherscan.io/address/${address}`;
|
||||
// Если есть chainId, ищем информацию о сети в supportedNetworks
|
||||
if (chainId && supportedNetworks.value.length > 0) {
|
||||
const network = supportedNetworks.value.find(n => n.chainId === chainId);
|
||||
if (network && network.etherscanUrl) {
|
||||
return `${network.etherscanUrl}/address/${address}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback на старую логику по networkIndex (для обратной совместимости)
|
||||
@@ -1204,6 +1431,102 @@ onMounted(() => {
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Статус деплоя */
|
||||
.deployment-status {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.status-loading i {
|
||||
color: #007bff;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.status-loading span {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.status-message.completed {
|
||||
background-color: #e8f5e8;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.status-message.in_progress {
|
||||
background-color: #e3f2fd;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.status-message.failed {
|
||||
background-color: #ffebee;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.status-message.not_started {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.status-message.unknown {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 2rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.status-message.completed .status-icon {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-message.in_progress .status-icon {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.status-message.failed .status-icon {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.status-message.not_started .status-icon {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-message.unknown .status-icon {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.status-content h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-content p {
|
||||
margin: 0;
|
||||
color: #6c757d;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
<!--
|
||||
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
All rights reserved.
|
||||
|
||||
This software is proprietary and confidential.
|
||||
Unauthorized copying, modification, or distribution is prohibited.
|
||||
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<BaseLayout
|
||||
:is-authenticated="isAuthenticated"
|
||||
:identities="identities"
|
||||
:token-balances="tokenBalances"
|
||||
:is-loading-tokens="isLoadingTokens"
|
||||
@auth-action-completed="$emit('auth-action-completed')"
|
||||
>
|
||||
<div class="reader-module-deploy">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Деплой DLEReader</h1>
|
||||
<p>API для чтения данных DLE - получение информации о контракте и предложениях</p>
|
||||
<p v-if="dleAddress" class="dle-address">
|
||||
<strong>DLE:</strong> {{ dleAddress }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="router.push('/management/modules')">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Информация о модуле -->
|
||||
<div class="module-info">
|
||||
<div class="info-card">
|
||||
<h3>📊 DLEReader</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<strong>Назначение:</strong> Чтение данных DLE контракта
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Функции:</strong> API для предложений, голосования, статистики
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Безопасность:</strong> Только чтение, не изменяет состояние
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма деплоя модуля во всех сетях -->
|
||||
<div class="deploy-form">
|
||||
<div class="form-header">
|
||||
<h3>🌐 Деплой DLEReader во всех сетях</h3>
|
||||
<p>Деплой API модуля для чтения данных во всех 4 сетях одновременно</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- Информация о сетях -->
|
||||
<div class="networks-info">
|
||||
<h4>📡 Сети для деплоя:</h4>
|
||||
<div class="networks-list">
|
||||
<div class="network-item">
|
||||
<span class="network-name">Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 11155111</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Holesky</span>
|
||||
<span class="network-chain-id">Chain ID: 17000</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Arbitrum Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 421614</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Base Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 84532</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки модуля -->
|
||||
<div class="module-settings">
|
||||
<h4>⚙️ Настройки DLEReader:</h4>
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="chainId">ID сети:</label>
|
||||
<select
|
||||
id="chainId"
|
||||
v-model="moduleSettings.chainId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="11155111">Sepolia (11155111)</option>
|
||||
<option value="17000">Holesky (17000)</option>
|
||||
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||
<option value="84532">Base Sepolia (84532)</option>
|
||||
</select>
|
||||
<small class="form-help">ID сети для деплоя модуля</small>
|
||||
</div>
|
||||
|
||||
<div class="simple-info">
|
||||
<h5>📋 Информация о DLEReader:</h5>
|
||||
<div class="info-text">
|
||||
<p><strong>DLEReader</strong> - это простой read-only модуль, который:</p>
|
||||
<ul>
|
||||
<li>✅ Только читает данные из DLE контракта</li>
|
||||
<li>✅ Не изменяет состояние блокчейна</li>
|
||||
<li>✅ Предоставляет API для получения информации</li>
|
||||
<li>✅ Безопасен для обновления</li>
|
||||
</ul>
|
||||
<p><strong>Конструктор принимает только один параметр:</strong> адрес DLE контракта</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка деплоя -->
|
||||
<div class="deploy-actions">
|
||||
<button
|
||||
class="btn btn-primary btn-large deploy-module"
|
||||
@click="deployDLEReader"
|
||||
:disabled="isDeploying || !dleAddress"
|
||||
>
|
||||
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||
{{ isDeploying ? 'Деплой модуля...' : 'Деплой DLEReader' }}
|
||||
</button>
|
||||
|
||||
<div v-if="deploymentProgress" class="deployment-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ deploymentProgress.message }}</span>
|
||||
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../../components/BaseLayout.vue';
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Состояние
|
||||
const isLoading = ref(false);
|
||||
const dleAddress = ref(route.query.address || null);
|
||||
const isDeploying = ref(false);
|
||||
const deploymentProgress = ref(null);
|
||||
|
||||
// Настройки модуля
|
||||
const moduleSettings = ref({
|
||||
// Единственный параметр - ID сети
|
||||
chainId: 11155111
|
||||
});
|
||||
|
||||
// Функция деплоя DLEReader
|
||||
async function deployDLEReader() {
|
||||
try {
|
||||
isDeploying.value = true;
|
||||
deploymentProgress.value = {
|
||||
message: 'Инициализация деплоя...',
|
||||
percentage: 0
|
||||
};
|
||||
|
||||
console.log('[DLEReaderDeployView] Начинаем деплой DLEReader для DLE:', dleAddress.value);
|
||||
|
||||
// Вызываем API для деплоя модуля во всех сетях
|
||||
const response = await fetch('/api/dle-modules/deploy-reader', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
dleAddress: dleAddress.value,
|
||||
moduleType: 'reader',
|
||||
settings: {
|
||||
// Единственный параметр - ID сети
|
||||
chainId: moduleSettings.value.chainId
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('[DLEReaderDeployView] Деплой успешно запущен:', result);
|
||||
|
||||
// Обновляем прогресс
|
||||
deploymentProgress.value = {
|
||||
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
|
||||
percentage: 100
|
||||
};
|
||||
|
||||
alert('✅ Деплой DLEReader запущен во всех сетях!');
|
||||
|
||||
// Перенаправляем обратно к модулям
|
||||
setTimeout(() => {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Неизвестная ошибка');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[DLEReaderDeployView] Ошибка деплоя:', error);
|
||||
alert('❌ Ошибка деплоя: ' + error.message);
|
||||
|
||||
deploymentProgress.value = {
|
||||
message: 'Ошибка деплоя: ' + error.message,
|
||||
percentage: 0
|
||||
};
|
||||
} finally {
|
||||
isDeploying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
console.log('[DLEReaderDeployView] Страница загружена');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reader-module-deploy {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: var(--radius-lg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-content p {
|
||||
margin: 0 0 5px 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.dle-address {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Информация о модуле */
|
||||
.module-info {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.info-item strong {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Форма деплоя */
|
||||
.deploy-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-header h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
margin: 0 0 20px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.networks-info,
|
||||
.module-settings {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Настройки отображения данных */
|
||||
.data-display-settings {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.data-display-settings h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.data-display-settings .form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.data-display-settings .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.data-display-settings .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Простая информация */
|
||||
.simple-info {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.simple-info h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.info-text p {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.info-text ul {
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.info-text li {
|
||||
margin: 5px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.info-text strong {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.deploy-actions {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 16px 32px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.deployment-progress {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Сети */
|
||||
.networks-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.network-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.network-chain-id {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -49,11 +49,255 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма деплоя будет добавлена позже -->
|
||||
<div class="deploy-form-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<h3>🚧 Форма деплоя в разработке</h3>
|
||||
<p>Здесь будет форма для деплоя TimelockModule</p>
|
||||
<!-- Форма деплоя модуля во всех сетях -->
|
||||
<div class="deploy-form">
|
||||
<div class="form-header">
|
||||
<h3>🌐 Деплой TimelockModule во всех сетях</h3>
|
||||
<p>Деплой модуля временных задержек во всех 4 сетях одновременно</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- Информация о сетях -->
|
||||
<div class="networks-info">
|
||||
<h4>📡 Сети для деплоя:</h4>
|
||||
<div class="networks-list">
|
||||
<div class="network-item">
|
||||
<span class="network-name">Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 11155111</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Holesky</span>
|
||||
<span class="network-chain-id">Chain ID: 17000</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Arbitrum Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 421614</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Base Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 84532</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки модуля -->
|
||||
<div class="module-settings">
|
||||
<h4>⚙️ Настройки TimelockModule:</h4>
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="chainId">ID сети:</label>
|
||||
<select
|
||||
id="chainId"
|
||||
v-model="moduleSettings.chainId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="11155111">Sepolia (11155111)</option>
|
||||
<option value="17000">Holesky (17000)</option>
|
||||
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||
<option value="84532">Base Sepolia (84532)</option>
|
||||
</select>
|
||||
<small class="form-help">ID сети для деплоя модуля</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="defaultDelay">Стандартная задержка (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="defaultDelay"
|
||||
v-model="moduleSettings.defaultDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="30"
|
||||
placeholder="2"
|
||||
>
|
||||
<small class="form-help">Стандартная задержка для операций (1-30 дней)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="emergencyDelay"
|
||||
v-model="moduleSettings.emergencyDelay"
|
||||
class="form-control"
|
||||
min="5"
|
||||
max="1440"
|
||||
placeholder="30"
|
||||
>
|
||||
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxDelay">Максимальная задержка (дни):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxDelay"
|
||||
v-model="moduleSettings.maxDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="365"
|
||||
placeholder="30"
|
||||
>
|
||||
<small class="form-help">Максимальная задержка для операций (1-365 дней)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="minDelay">Минимальная задержка (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="minDelay"
|
||||
v-model="moduleSettings.minDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="720"
|
||||
placeholder="24"
|
||||
>
|
||||
<small class="form-help">Минимальная задержка для операций (1-720 часов)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxOperations">Максимум операций в очереди:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxOperations"
|
||||
v-model="moduleSettings.maxOperations"
|
||||
class="form-control"
|
||||
min="10"
|
||||
max="1000"
|
||||
placeholder="100"
|
||||
>
|
||||
<small class="form-help">Максимальное количество операций в очереди (10-1000)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные настройки таймлока -->
|
||||
<div class="advanced-settings">
|
||||
<h5>🔧 Дополнительные настройки таймлока:</h5>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="criticalOperations">Критические операции (JSON формат):</label>
|
||||
<textarea
|
||||
id="criticalOperations"
|
||||
v-model="moduleSettings.criticalOperations"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder='["0x12345678", "0x87654321"]'
|
||||
></textarea>
|
||||
<small class="form-help">Селекторы функций, которые считаются критическими (JSON массив)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emergencyOperations">Экстренные операции (JSON формат):</label>
|
||||
<textarea
|
||||
id="emergencyOperations"
|
||||
v-model="moduleSettings.emergencyOperations"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder='["0xabcdef12", "0x21fedcba"]'
|
||||
></textarea>
|
||||
<small class="form-help">Селекторы функций для экстренных операций (JSON массив)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="operationDelays">Задержки для операций (JSON формат):</label>
|
||||
<textarea
|
||||
id="operationDelays"
|
||||
v-model="moduleSettings.operationDelays"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder='{"0x12345678": 86400, "0x87654321": 172800}'
|
||||
></textarea>
|
||||
<small class="form-help">Кастомные задержки для конкретных операций (селектор => секунды)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="autoExecuteEnabled">Автоисполнение включено:</label>
|
||||
<select
|
||||
id="autoExecuteEnabled"
|
||||
v-model="moduleSettings.autoExecuteEnabled"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Отключено</option>
|
||||
</select>
|
||||
<small class="form-help">Автоматическое исполнение операций после истечения задержки</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="cancellationWindow">Окно отмены (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="cancellationWindow"
|
||||
v-model="moduleSettings.cancellationWindow"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="168"
|
||||
placeholder="24"
|
||||
>
|
||||
<small class="form-help">Время, в течение которого можно отменить операцию (1-168 часов)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="executionWindow">Окно исполнения (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="executionWindow"
|
||||
v-model="moduleSettings.executionWindow"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="168"
|
||||
placeholder="48"
|
||||
>
|
||||
<small class="form-help">Время, в течение которого можно исполнить операцию (1-168 часов)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timelockDescription">Описание таймлока:</label>
|
||||
<textarea
|
||||
id="timelockDescription"
|
||||
v-model="moduleSettings.timelockDescription"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="Описание таймлока DLE для безопасности операций..."
|
||||
></textarea>
|
||||
<small class="form-help">Описание таймлока для документации</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка деплоя -->
|
||||
<div class="deploy-actions">
|
||||
<button
|
||||
class="btn btn-primary btn-large deploy-module"
|
||||
@click="deployTimelockModule"
|
||||
:disabled="isDeploying || !dleAddress"
|
||||
>
|
||||
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TimelockModule' }}
|
||||
</button>
|
||||
|
||||
<div v-if="deploymentProgress" class="deployment-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ deploymentProgress.message }}</span>
|
||||
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +327,108 @@ const route = useRoute();
|
||||
// Состояние
|
||||
const isLoading = ref(false);
|
||||
const dleAddress = ref(route.query.address || null);
|
||||
const isDeploying = ref(false);
|
||||
const deploymentProgress = ref(null);
|
||||
|
||||
// Настройки модуля
|
||||
const moduleSettings = ref({
|
||||
// Основные параметры
|
||||
chainId: 11155111,
|
||||
defaultDelay: 2, // days
|
||||
emergencyDelay: 30, // minutes
|
||||
maxDelay: 30, // days
|
||||
minDelay: 24, // hours
|
||||
|
||||
// Дополнительные настройки
|
||||
maxOperations: 100,
|
||||
criticalOperations: '',
|
||||
emergencyOperations: '',
|
||||
operationDelays: '',
|
||||
autoExecuteEnabled: 'true',
|
||||
cancellationWindow: 24, // hours
|
||||
executionWindow: 48, // hours
|
||||
timelockDescription: ''
|
||||
});
|
||||
|
||||
// Функция деплоя TimelockModule
|
||||
async function deployTimelockModule() {
|
||||
try {
|
||||
isDeploying.value = true;
|
||||
deploymentProgress.value = {
|
||||
message: 'Инициализация деплоя...',
|
||||
percentage: 0
|
||||
};
|
||||
|
||||
console.log('[TimelockModuleDeployView] Начинаем деплой TimelockModule для DLE:', dleAddress.value);
|
||||
|
||||
// Вызываем API для деплоя модуля во всех сетях
|
||||
const response = await fetch('/api/dle-modules/deploy-timelock', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
dleAddress: dleAddress.value,
|
||||
moduleType: 'timelock',
|
||||
settings: {
|
||||
// Основные параметры
|
||||
chainId: moduleSettings.value.chainId,
|
||||
defaultDelay: moduleSettings.value.defaultDelay * 24 * 60 * 60, // конвертируем дни в секунды
|
||||
emergencyDelay: moduleSettings.value.emergencyDelay * 60, // конвертируем минуты в секунды
|
||||
maxDelay: moduleSettings.value.maxDelay * 24 * 60 * 60, // конвертируем дни в секунды
|
||||
minDelay: moduleSettings.value.minDelay * 60 * 60, // конвертируем часы в секунды
|
||||
|
||||
// Дополнительные настройки
|
||||
maxOperations: parseInt(moduleSettings.value.maxOperations),
|
||||
criticalOperations: moduleSettings.value.criticalOperations ? JSON.parse(moduleSettings.value.criticalOperations) : [],
|
||||
emergencyOperations: moduleSettings.value.emergencyOperations ? JSON.parse(moduleSettings.value.emergencyOperations) : [],
|
||||
operationDelays: moduleSettings.value.operationDelays ? JSON.parse(moduleSettings.value.operationDelays) : {},
|
||||
autoExecuteEnabled: moduleSettings.value.autoExecuteEnabled === 'true',
|
||||
cancellationWindow: moduleSettings.value.cancellationWindow * 60 * 60, // конвертируем часы в секунды
|
||||
executionWindow: moduleSettings.value.executionWindow * 60 * 60, // конвертируем часы в секунды
|
||||
timelockDescription: moduleSettings.value.timelockDescription
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('[TimelockModuleDeployView] Деплой успешно запущен:', result);
|
||||
|
||||
// Обновляем прогресс
|
||||
deploymentProgress.value = {
|
||||
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
|
||||
percentage: 100
|
||||
};
|
||||
|
||||
alert('✅ Деплой TimelockModule запущен во всех сетях!');
|
||||
|
||||
// Перенаправляем обратно к модулям
|
||||
setTimeout(() => {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Неизвестная ошибка');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[TimelockModuleDeployView] Ошибка деплоя:', error);
|
||||
alert('❌ Ошибка деплоя: ' + error.message);
|
||||
|
||||
deploymentProgress.value = {
|
||||
message: 'Ошибка деплоя: ' + error.message,
|
||||
percentage: 0
|
||||
};
|
||||
} finally {
|
||||
isDeploying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
@@ -150,6 +496,182 @@ onMounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Форма деплоя */
|
||||
.deploy-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-header h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
margin: 0 0 20px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.networks-info,
|
||||
.module-settings {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Дополнительные настройки */
|
||||
.advanced-settings {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.advanced-settings h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.advanced-settings .form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.advanced-settings .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.advanced-settings .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.deploy-actions {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 16px 32px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.deployment-progress {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Информация о модуле */
|
||||
.module-info {
|
||||
margin-bottom: 30px;
|
||||
|
||||
@@ -49,11 +49,263 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Форма деплоя будет добавлена позже -->
|
||||
<div class="deploy-form-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<h3>🚧 Форма деплоя в разработке</h3>
|
||||
<p>Здесь будет форма для деплоя TreasuryModule</p>
|
||||
<!-- Форма деплоя модуля во всех сетях -->
|
||||
<div class="deploy-form">
|
||||
<div class="form-header">
|
||||
<h3>🌐 Деплой TreasuryModule во всех сетях</h3>
|
||||
<p>Деплой модуля казначейства во всех 4 сетях одновременно</p>
|
||||
</div>
|
||||
|
||||
<div class="form-content">
|
||||
<!-- Информация о сетях -->
|
||||
<div class="networks-info">
|
||||
<h4>📡 Сети для деплоя:</h4>
|
||||
<div class="networks-list">
|
||||
<div class="network-item">
|
||||
<span class="network-name">Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 11155111</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Holesky</span>
|
||||
<span class="network-chain-id">Chain ID: 17000</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Arbitrum Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 421614</span>
|
||||
</div>
|
||||
<div class="network-item">
|
||||
<span class="network-name">Base Sepolia</span>
|
||||
<span class="network-chain-id">Chain ID: 84532</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Настройки модуля -->
|
||||
<div class="module-settings">
|
||||
<h4>⚙️ Настройки TreasuryModule:</h4>
|
||||
|
||||
<div class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="emergencyAdmin">Адрес экстренного администратора:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="emergencyAdmin"
|
||||
v-model="moduleSettings.emergencyAdmin"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
required
|
||||
>
|
||||
<small class="form-help">Адрес экстренного администратора для управления модулем</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="chainId">ID сети:</label>
|
||||
<select
|
||||
id="chainId"
|
||||
v-model="moduleSettings.chainId"
|
||||
class="form-control"
|
||||
required
|
||||
>
|
||||
<option value="11155111">Sepolia (11155111)</option>
|
||||
<option value="17000">Holesky (17000)</option>
|
||||
<option value="421614">Arbitrum Sepolia (421614)</option>
|
||||
<option value="84532">Base Sepolia (84532)</option>
|
||||
</select>
|
||||
<small class="form-help">ID сети для деплоя модуля</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="defaultDelay">Стандартная задержка (часы):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="defaultDelay"
|
||||
v-model="moduleSettings.defaultDelay"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="720"
|
||||
placeholder="24"
|
||||
>
|
||||
<small class="form-help">Стандартная задержка для операций (1-720 часов)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emergencyDelay">Экстренная задержка (минуты):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="emergencyDelay"
|
||||
v-model="moduleSettings.emergencyDelay"
|
||||
class="form-control"
|
||||
min="5"
|
||||
max="1440"
|
||||
placeholder="30"
|
||||
>
|
||||
<small class="form-help">Экстренная задержка для критических операций (5-1440 минут)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="supportedTokens">Поддерживаемые токены (адреса через запятую):</label>
|
||||
<textarea
|
||||
id="supportedTokens"
|
||||
v-model="moduleSettings.supportedTokens"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder="0x1234..., 0x5678..., 0x9abc..."
|
||||
></textarea>
|
||||
<small class="form-help">Адреса ERC20 токенов, которые будет поддерживать казначейство (через запятую)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gasPaymentTokens">Токены для оплаты газа (адреса через запятую):</label>
|
||||
<textarea
|
||||
id="gasPaymentTokens"
|
||||
v-model="moduleSettings.gasPaymentTokens"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="0x1234..., 0x5678..."
|
||||
></textarea>
|
||||
<small class="form-help">Токены, которыми можно оплачивать газ (через запятую)</small>
|
||||
</div>
|
||||
|
||||
<!-- Дополнительные настройки казны -->
|
||||
<div class="advanced-settings">
|
||||
<h5>🔧 Дополнительные настройки казны:</h5>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="paymasterAddress">Адрес Paymaster:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="paymasterAddress"
|
||||
v-model="moduleSettings.paymasterAddress"
|
||||
class="form-control"
|
||||
placeholder="0x..."
|
||||
>
|
||||
<small class="form-help">Адрес Paymaster для ERC-4337 (оплата газа любым токеном)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="maxBatchTransfers">Максимум batch переводов:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxBatchTransfers"
|
||||
v-model="moduleSettings.maxBatchTransfers"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="50"
|
||||
>
|
||||
<small class="form-help">Максимальное количество переводов в batch операции (1-100)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="gasTokenRates">Курсы токенов для газа (JSON формат):</label>
|
||||
<textarea
|
||||
id="gasTokenRates"
|
||||
v-model="moduleSettings.gasTokenRates"
|
||||
class="form-control"
|
||||
rows="3"
|
||||
placeholder='{"0x1234...": "1000000000000000000", "0x5678...": "2000000000000000000"}'
|
||||
></textarea>
|
||||
<small class="form-help">Курсы обмена токенов на нативную монету (JSON формат)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emergencyThreshold">Порог экстренных операций (ETH):</label>
|
||||
<input
|
||||
type="number"
|
||||
id="emergencyThreshold"
|
||||
v-model="moduleSettings.emergencyThreshold"
|
||||
class="form-control"
|
||||
min="0"
|
||||
step="0.001"
|
||||
placeholder="1.0"
|
||||
>
|
||||
<small class="form-help">Порог для экстренных операций в ETH</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="initialTokens">Начальные токены для добавления (JSON формат):</label>
|
||||
<textarea
|
||||
id="initialTokens"
|
||||
v-model="moduleSettings.initialTokens"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder='[{"address": "0x1234...", "symbol": "USDC", "decimals": 6}, {"address": "0x5678...", "symbol": "USDT", "decimals": 6}]'
|
||||
></textarea>
|
||||
<small class="form-help">Токены для автоматического добавления при деплое (JSON массив)</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="autoRefreshBalances">Автообновление балансов:</label>
|
||||
<select
|
||||
id="autoRefreshBalances"
|
||||
v-model="moduleSettings.autoRefreshBalances"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Отключено</option>
|
||||
</select>
|
||||
<small class="form-help">Автоматическое обновление балансов токенов</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="batchTransferEnabled">Batch переводы включены:</label>
|
||||
<select
|
||||
id="batchTransferEnabled"
|
||||
v-model="moduleSettings.batchTransferEnabled"
|
||||
class="form-control"
|
||||
>
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Отключено</option>
|
||||
</select>
|
||||
<small class="form-help">Разрешить batch операции переводов</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="treasuryDescription">Описание казны:</label>
|
||||
<textarea
|
||||
id="treasuryDescription"
|
||||
v-model="moduleSettings.treasuryDescription"
|
||||
class="form-control"
|
||||
rows="2"
|
||||
placeholder="Описание казны DLE для управления финансами..."
|
||||
></textarea>
|
||||
<small class="form-help">Описание казны для документации</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопка деплоя -->
|
||||
<div class="deploy-actions">
|
||||
<button
|
||||
class="btn btn-primary btn-large deploy-module"
|
||||
@click="deployTreasuryModule"
|
||||
:disabled="isDeploying || !dleAddress"
|
||||
>
|
||||
<i class="fas fa-rocket" :class="{ 'fa-spin': isDeploying }"></i>
|
||||
{{ isDeploying ? 'Деплой модуля...' : 'Деплой TreasuryModule' }}
|
||||
</button>
|
||||
|
||||
<div v-if="deploymentProgress" class="deployment-progress">
|
||||
<div class="progress-info">
|
||||
<span>{{ deploymentProgress.message }}</span>
|
||||
<span class="progress-percentage">{{ deploymentProgress.percentage }}%</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: deploymentProgress.percentage + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +335,114 @@ const route = useRoute();
|
||||
// Состояние
|
||||
const isLoading = ref(false);
|
||||
const dleAddress = ref(route.query.address || null);
|
||||
const isDeploying = ref(false);
|
||||
const deploymentProgress = ref(null);
|
||||
|
||||
// Настройки модуля
|
||||
const moduleSettings = ref({
|
||||
// Основные параметры
|
||||
emergencyAdmin: '',
|
||||
chainId: 11155111,
|
||||
defaultDelay: 24, // hours
|
||||
emergencyDelay: 30, // minutes
|
||||
|
||||
// Токены
|
||||
supportedTokens: '',
|
||||
gasPaymentTokens: '',
|
||||
initialTokens: '',
|
||||
|
||||
// Дополнительные настройки
|
||||
paymasterAddress: '',
|
||||
maxBatchTransfers: 50,
|
||||
gasTokenRates: '',
|
||||
emergencyThreshold: 1.0,
|
||||
autoRefreshBalances: 'true',
|
||||
batchTransferEnabled: 'true',
|
||||
treasuryDescription: ''
|
||||
});
|
||||
|
||||
// Функция деплоя TreasuryModule
|
||||
async function deployTreasuryModule() {
|
||||
try {
|
||||
isDeploying.value = true;
|
||||
deploymentProgress.value = {
|
||||
message: 'Инициализация деплоя...',
|
||||
percentage: 0
|
||||
};
|
||||
|
||||
console.log('[TreasuryModuleDeployView] Начинаем деплой TreasuryModule для DLE:', dleAddress.value);
|
||||
|
||||
// Вызываем API для деплоя модуля во всех сетях
|
||||
const response = await fetch('/api/dle-modules/deploy-treasury', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
dleAddress: dleAddress.value,
|
||||
moduleType: 'treasury',
|
||||
settings: {
|
||||
// Основные параметры
|
||||
emergencyAdmin: moduleSettings.value.emergencyAdmin,
|
||||
chainId: moduleSettings.value.chainId,
|
||||
defaultDelay: moduleSettings.value.defaultDelay,
|
||||
emergencyDelay: moduleSettings.value.emergencyDelay,
|
||||
|
||||
// Токены
|
||||
supportedTokens: moduleSettings.value.supportedTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
|
||||
gasPaymentTokens: moduleSettings.value.gasPaymentTokens.split(',').map(addr => addr.trim()).filter(addr => addr),
|
||||
initialTokens: moduleSettings.value.initialTokens ? JSON.parse(moduleSettings.value.initialTokens) : [],
|
||||
|
||||
// Дополнительные настройки
|
||||
paymasterAddress: moduleSettings.value.paymasterAddress,
|
||||
maxBatchTransfers: parseInt(moduleSettings.value.maxBatchTransfers),
|
||||
gasTokenRates: moduleSettings.value.gasTokenRates ? JSON.parse(moduleSettings.value.gasTokenRates) : {},
|
||||
emergencyThreshold: parseFloat(moduleSettings.value.emergencyThreshold),
|
||||
autoRefreshBalances: moduleSettings.value.autoRefreshBalances === 'true',
|
||||
batchTransferEnabled: moduleSettings.value.batchTransferEnabled === 'true',
|
||||
treasuryDescription: moduleSettings.value.treasuryDescription
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
console.log('[TreasuryModuleDeployView] Деплой успешно запущен:', result);
|
||||
|
||||
// Обновляем прогресс
|
||||
deploymentProgress.value = {
|
||||
message: 'Деплой запущен успешно! Проверьте логи для отслеживания прогресса.',
|
||||
percentage: 100
|
||||
};
|
||||
|
||||
alert('✅ Деплой TreasuryModule запущен во всех сетях!');
|
||||
|
||||
// Перенаправляем обратно к модулям
|
||||
setTimeout(() => {
|
||||
router.push(`/management/modules?address=${dleAddress.value}`);
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
throw new Error(result.error || 'Неизвестная ошибка');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[TreasuryModuleDeployView] Ошибка деплоя:', error);
|
||||
alert('❌ Ошибка деплоя: ' + error.message);
|
||||
|
||||
deploymentProgress.value = {
|
||||
message: 'Ошибка деплоя: ' + error.message,
|
||||
percentage: 0
|
||||
};
|
||||
} finally {
|
||||
isDeploying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
onMounted(() => {
|
||||
@@ -150,6 +510,240 @@ onMounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Форма деплоя */
|
||||
.deploy-form {
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-header h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
margin: 0 0 20px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.networks-info,
|
||||
.module-settings {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Дополнительные настройки */
|
||||
.advanced-settings {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.advanced-settings h5 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.advanced-settings .form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.advanced-settings .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.advanced-settings .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.networks-info h4,
|
||||
.deploy-parameters h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.networks-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.network-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.network-chain-id {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.parameter-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.parameter-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.parameter-item label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.parameter-value {
|
||||
font-family: monospace;
|
||||
color: var(--color-primary);
|
||||
background: #f8f9fa;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.deploy-actions {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark));
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, var(--color-primary-dark), var(--color-primary));
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 16px 32px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.deployment-progress {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-dark));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Информация о модуле */
|
||||
.module-info {
|
||||
margin-bottom: 30px;
|
||||
|
||||
27
frontend/src/views/smartcontracts/modules/index.js
Normal file
27
frontend/src/views/smartcontracts/modules/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
// Основные модули DLE
|
||||
export { default as TreasuryModuleDeployView } from './TreasuryModuleDeployView.vue';
|
||||
export { default as TimelockModuleDeployView } from './TimelockModuleDeployView.vue';
|
||||
export { default as DLEReaderDeployView } from './DLEReaderDeployView.vue';
|
||||
|
||||
// Дополнительные модули
|
||||
export { default as CommunicationModuleDeployView } from './CommunicationModuleDeployView.vue';
|
||||
export { default as ApplicationModuleDeployView } from './ApplicationModuleDeployView.vue';
|
||||
export { default as MintModuleDeploy } from './MintModuleDeploy.vue';
|
||||
export { default as BurnModuleDeploy } from './BurnModuleDeploy.vue';
|
||||
export { default as OracleModuleDeploy } from './OracleModuleDeploy.vue';
|
||||
export { default as InheritanceModuleDeploy } from './InheritanceModuleDeploy.vue';
|
||||
|
||||
// Кастомный модуль
|
||||
export { default as ModuleDeployFormView } from './ModuleDeployFormView.vue';
|
||||
Reference in New Issue
Block a user