ваше сообщение коммита
This commit is contained in:
@@ -53,6 +53,7 @@ import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits, provi
|
||||
import { useAuthContext } from '../composables/useAuth';
|
||||
import { useAuthFlow } from '../composables/useAuthFlow';
|
||||
import { useNotifications } from '../composables/useNotifications';
|
||||
import { useTokenBalancesWebSocket } from '../composables/useTokenBalancesWebSocket';
|
||||
import { getFromStorage, setToStorage, removeFromStorage } from '../utils/storage';
|
||||
import { connectWithWallet } from '../services/wallet';
|
||||
import api from '../api/axios';
|
||||
@@ -68,7 +69,10 @@ import NotificationDisplay from './NotificationDisplay.vue';
|
||||
const auth = useAuthContext();
|
||||
const { notifications, showSuccessMessage, showErrorMessage } = useNotifications();
|
||||
|
||||
// Определяем props, которые будут приходить от родительского View
|
||||
// Используем useTokenBalancesWebSocket для получения актуального состояния загрузки
|
||||
const { isLoadingTokens: wsIsLoadingTokens } = useTokenBalancesWebSocket();
|
||||
|
||||
// Определяем props, которые будут приходить от родительского View (оставляем для совместимости)
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
@@ -79,17 +83,26 @@ const props = defineProps({
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
// Используем useAuth напрямую для получения актуальных данных
|
||||
const isAuthenticated = computed(() => auth.isAuthenticated.value);
|
||||
const identities = computed(() => auth.identities.value);
|
||||
const tokenBalances = computed(() => auth.tokenBalances.value);
|
||||
const isLoadingTokens = computed(() => {
|
||||
// Приоритет: WebSocket состояние > пропс > false
|
||||
return wsIsLoadingTokens.value || (props.isLoadingTokens !== undefined ? props.isLoadingTokens : false);
|
||||
});
|
||||
|
||||
// Предоставляем данные дочерним компонентам через provide/inject
|
||||
provide('isAuthenticated', computed(() => props.isAuthenticated));
|
||||
provide('identities', computed(() => props.identities));
|
||||
provide('tokenBalances', computed(() => props.tokenBalances));
|
||||
provide('isLoadingTokens', computed(() => props.isLoadingTokens));
|
||||
provide('isAuthenticated', isAuthenticated);
|
||||
provide('identities', identities);
|
||||
provide('tokenBalances', tokenBalances);
|
||||
provide('isLoadingTokens', isLoadingTokens);
|
||||
|
||||
// Отладочная информация
|
||||
console.log('[BaseLayout] Props received:', {
|
||||
isAuthenticated: props.isAuthenticated,
|
||||
tokenBalances: props.tokenBalances,
|
||||
isLoadingTokens: props.isLoadingTokens
|
||||
console.log('[BaseLayout] Auth state:', {
|
||||
isAuthenticated: isAuthenticated.value,
|
||||
tokenBalances: tokenBalances.value,
|
||||
isLoadingTokens: isLoadingTokens.value
|
||||
});
|
||||
|
||||
// Callback после успешной аутентификации/привязки через Email/Telegram
|
||||
@@ -168,6 +181,12 @@ const handleWalletAuth = async () => {
|
||||
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
|
||||
} else if (error.message && error.message.includes('Браузерный кошелек не установлен')) {
|
||||
errorMessage = 'Браузерный кошелек не установлен. Пожалуйста, установите MetaMask.';
|
||||
} else if (error.message && error.message.includes('Не удалось получить nonce')) {
|
||||
errorMessage = 'Ошибка получения nonce. Попробуйте обновить страницу и повторить попытку.';
|
||||
} else if (error.message && error.message.includes('Invalid nonce')) {
|
||||
errorMessage = 'Ошибка аутентификации. Попробуйте обновить страницу и повторить попытку.';
|
||||
} else if (error.message && error.message.includes('Nonce expired')) {
|
||||
errorMessage = 'Время сессии истекло. Попробуйте обновить страницу и повторить попытку.';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
223
frontend/src/components/NetworkSwitchNotification.vue
Normal file
223
frontend/src/components/NetworkSwitchNotification.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<!--
|
||||
Network Switch Notification Component
|
||||
Компонент для уведомления о необходимости переключения сети
|
||||
|
||||
Author: HB3 Accelerator
|
||||
For licensing inquiries: info@hb3-accelerator.com
|
||||
Website: https://hb3-accelerator.com
|
||||
GitHub: https://github.com/HB3-ACCELERATOR
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="showNotification" class="network-notification">
|
||||
<div class="notification-content">
|
||||
<div class="notification-icon">⚠️</div>
|
||||
<div class="notification-text">
|
||||
<h4>Требуется переключение сети</h4>
|
||||
<p>Для голосования по этому предложению необходимо переключиться на сеть <strong>{{ targetNetworkName }}</strong></p>
|
||||
<p>Текущая сеть: <strong>{{ currentNetworkName }}</strong></p>
|
||||
</div>
|
||||
<div class="notification-actions">
|
||||
<button @click="switchNetwork" class="btn btn-primary" :disabled="isSwitching">
|
||||
{{ isSwitching ? 'Переключение...' : 'Переключить сеть' }}
|
||||
</button>
|
||||
<button @click="dismiss" class="btn btn-secondary">Позже</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue';
|
||||
import { switchNetwork, getCurrentNetwork } from '@/utils/networkSwitcher';
|
||||
|
||||
export default {
|
||||
name: 'NetworkSwitchNotification',
|
||||
props: {
|
||||
targetChainId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
currentChainId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['network-switched', 'dismissed'],
|
||||
setup(props, { emit }) {
|
||||
const isSwitching = ref(false);
|
||||
const showNotification = computed(() => props.visible && props.targetChainId !== props.currentChainId);
|
||||
|
||||
const targetNetworkName = computed(() => {
|
||||
const networkNames = {
|
||||
1: 'Ethereum Mainnet',
|
||||
11155111: 'Sepolia',
|
||||
17000: 'Holesky',
|
||||
421614: 'Arbitrum Sepolia',
|
||||
84532: 'Base Sepolia',
|
||||
8453: 'Base'
|
||||
};
|
||||
return networkNames[props.targetChainId] || `Сеть ${props.targetChainId}`;
|
||||
});
|
||||
|
||||
const currentNetworkName = computed(() => {
|
||||
const networkNames = {
|
||||
1: 'Ethereum Mainnet',
|
||||
11155111: 'Sepolia',
|
||||
17000: 'Holesky',
|
||||
421614: 'Arbitrum Sepolia',
|
||||
84532: 'Base Sepolia',
|
||||
8453: 'Base'
|
||||
};
|
||||
return networkNames[props.currentChainId] || `Сеть ${props.currentChainId}`;
|
||||
});
|
||||
|
||||
const switchNetworkHandler = async () => {
|
||||
try {
|
||||
isSwitching.value = true;
|
||||
console.log(`🔄 [Network Switch] Переключаемся на сеть ${props.targetChainId}...`);
|
||||
|
||||
const result = await switchNetwork(props.targetChainId);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ [Network Switch] Сеть переключена успешно');
|
||||
emit('network-switched', result);
|
||||
} else {
|
||||
console.error('❌ [Network Switch] Ошибка переключения:', result.error);
|
||||
alert(`Ошибка переключения сети: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Network Switch] Ошибка:', error);
|
||||
alert(`Ошибка переключения сети: ${error.message}`);
|
||||
} finally {
|
||||
isSwitching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const dismiss = () => {
|
||||
emit('dismissed');
|
||||
};
|
||||
|
||||
return {
|
||||
showNotification,
|
||||
targetNetworkName,
|
||||
currentNetworkName,
|
||||
isSwitching,
|
||||
switchNetwork: switchNetworkHandler,
|
||||
dismiss
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.network-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #ddd;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notification-text h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.notification-text p {
|
||||
margin: 0 0 8px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.network-notification {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -192,5 +192,275 @@ const formatTime = (timestamp) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Ваши стили для формы */
|
||||
/* Статус подключения */
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.status-indicator.active {
|
||||
background-color: #28a745;
|
||||
box-shadow: 0 0 8px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.status-indicator.inactive {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.disconnect-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
/* Форма */
|
||||
.tunnel-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group textarea:disabled {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Дополнительные настройки */
|
||||
.advanced-section {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-start;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.publish-btn {
|
||||
background: linear-gradient(135deg, var(--color-primary), #20c997);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.publish-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #0056b3, #1ea085);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.publish-btn:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.reset-btn:hover:not(:disabled) {
|
||||
background: #5a6268;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.reset-btn:disabled {
|
||||
background: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Лог операций */
|
||||
.operation-log {
|
||||
margin-top: 2rem;
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.operation-log h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
min-width: 80px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-entry.success .log-message {
|
||||
color: #28a745;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-entry.error .log-message {
|
||||
color: #dc3545;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-entry.info .log-message {
|
||||
color: #17a2b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.publish-btn,
|
||||
.reset-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -30,6 +30,12 @@ const userAccessLevel = ref({ level: 'user', tokenCount: 0, hasAccess: false });
|
||||
const updateIdentities = async () => {
|
||||
if (!isAuthenticated.value || !userId.value) return;
|
||||
|
||||
// Проверяем, что identities ref существует
|
||||
if (!identities || typeof identities.value === 'undefined') {
|
||||
console.warn('Identities ref is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/auth/identities');
|
||||
if (response.data.success) {
|
||||
@@ -46,14 +52,26 @@ const updateIdentities = async () => {
|
||||
}, []);
|
||||
|
||||
// Сравниваем новый отфильтрованный список с текущим значением
|
||||
const currentProviders = identities.value.map(id => id.provider).sort();
|
||||
const newProviders = filteredIdentities.map(id => id.provider).sort();
|
||||
const currentProviders = (identities.value || []).map(id => id?.provider || '').sort();
|
||||
const newProviders = (filteredIdentities || []).map(id => id?.provider || '').sort();
|
||||
|
||||
const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders);
|
||||
|
||||
// Обновляем реактивное значение
|
||||
identities.value = filteredIdentities;
|
||||
console.log('User identities updated:', identities.value);
|
||||
// Обновляем реактивное значение с проверкой
|
||||
try {
|
||||
if (identities && identities.value !== undefined) {
|
||||
identities.value = filteredIdentities;
|
||||
console.log('User identities updated:', identities.value);
|
||||
} else {
|
||||
console.warn('Identities ref is not available or not initialized');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating identities:', error);
|
||||
// Если произошла ошибка, пытаемся инициализировать identities
|
||||
if (identities && typeof identities.value === 'undefined') {
|
||||
identities.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Если список идентификаторов изменился, принудительно проверяем аутентификацию,
|
||||
// чтобы обновить authType и другие связанные данные (например, telegramId)
|
||||
@@ -163,11 +181,21 @@ const updateAuth = async ({
|
||||
|
||||
// Обновляем идентификаторы при любом изменении аутентификации
|
||||
if (authenticated) {
|
||||
await updateIdentities();
|
||||
startIdentitiesPolling();
|
||||
try {
|
||||
await updateIdentities();
|
||||
startIdentitiesPolling();
|
||||
} catch (error) {
|
||||
console.error('Error updating identities in updateAuth:', error);
|
||||
}
|
||||
} else {
|
||||
stopIdentitiesPolling();
|
||||
identities.value = [];
|
||||
try {
|
||||
if (identities && typeof identities.value !== 'undefined') {
|
||||
identities.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error clearing identities:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Auth updated:', {
|
||||
@@ -306,7 +334,11 @@ const checkAuth = async () => {
|
||||
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
|
||||
if (response.data.authenticated) {
|
||||
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
|
||||
await updateIdentities();
|
||||
try {
|
||||
await updateIdentities();
|
||||
} catch (error) {
|
||||
console.error('Error updating identities in checkAuth:', error);
|
||||
}
|
||||
|
||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||
// связываем гостевые сообщения с его аккаунтом
|
||||
|
||||
349
frontend/src/composables/useDleContract.js
Normal file
349
frontend/src/composables/useDleContract.js
Normal file
@@ -0,0 +1,349 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { ethers } from 'ethers';
|
||||
import { DLE_ABI, TOKEN_ABI } from '@/utils/dle-abi';
|
||||
|
||||
/**
|
||||
* Композабл для работы с DLE смарт-контрактом
|
||||
* Содержит правильные ABI и функции для взаимодействия с контрактом
|
||||
*/
|
||||
export function useDleContract() {
|
||||
// Состояние
|
||||
const isConnected = ref(false);
|
||||
const provider = ref(null);
|
||||
const signer = ref(null);
|
||||
const contract = ref(null);
|
||||
const userAddress = ref(null);
|
||||
const chainId = ref(null);
|
||||
|
||||
// Используем общий ABI из utils/dle-abi.js
|
||||
|
||||
/**
|
||||
* Подключиться к кошельку
|
||||
*/
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||
}
|
||||
|
||||
// Запрашиваем подключение
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
|
||||
// Создаем провайдер и подписанта
|
||||
provider.value = new ethers.BrowserProvider(window.ethereum);
|
||||
signer.value = await provider.value.getSigner();
|
||||
userAddress.value = await signer.value.getAddress();
|
||||
|
||||
// Получаем информацию о сети
|
||||
const network = await provider.value.getNetwork();
|
||||
chainId.value = Number(network.chainId);
|
||||
|
||||
isConnected.value = true;
|
||||
|
||||
console.log('✅ Кошелек подключен:', {
|
||||
address: userAddress.value,
|
||||
chainId: chainId.value,
|
||||
network: network.name
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
address: userAddress.value,
|
||||
chainId: chainId.value
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка подключения к кошельку:', error);
|
||||
isConnected.value = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Инициализировать контракт
|
||||
*/
|
||||
const initContract = (contractAddress) => {
|
||||
if (!provider.value) {
|
||||
throw new Error('Провайдер не инициализирован. Сначала подключите кошелек.');
|
||||
}
|
||||
|
||||
contract.value = new ethers.Contract(contractAddress, DLE_ABI, signer.value);
|
||||
console.log('✅ DLE контракт инициализирован:', contractAddress);
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверить баланс токенов пользователя
|
||||
*/
|
||||
const checkTokenBalance = async (contractAddress) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const balance = await contract.value.balanceOf(userAddress.value);
|
||||
const balanceFormatted = ethers.formatEther(balance);
|
||||
|
||||
console.log(`💰 Баланс токенов: ${balanceFormatted}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
balance: balanceFormatted,
|
||||
hasTokens: balance > 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка проверки баланса:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
balance: '0',
|
||||
hasTokens: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Голосовать за предложение
|
||||
*/
|
||||
const voteOnProposal = async (contractAddress, proposalId, support) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
console.log('🗳️ Начинаем голосование:', { proposalId, support });
|
||||
|
||||
// Проверяем баланс токенов
|
||||
const balanceCheck = await checkTokenBalance(contractAddress);
|
||||
if (!balanceCheck.hasTokens) {
|
||||
throw new Error('У вас нет токенов для голосования');
|
||||
}
|
||||
|
||||
// Отправляем транзакцию голосования
|
||||
const tx = await contract.value.vote(proposalId, support);
|
||||
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||
|
||||
// Ждем подтверждения
|
||||
const receipt = await tx.wait();
|
||||
console.log('✅ Голосование успешно:', receipt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: tx.hash,
|
||||
receipt: receipt
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка голосования:', error);
|
||||
|
||||
// Улучшенная обработка ошибок
|
||||
let errorMessage = error.message;
|
||||
if (error.message.includes('execution reverted')) {
|
||||
errorMessage = 'Транзакция отклонена смарт-контрактом. Возможные причины:\n' +
|
||||
'• Предложение уже не активно\n' +
|
||||
'• Вы уже голосовали за это предложение\n' +
|
||||
'• Недостаточно прав для голосования\n' +
|
||||
'• Предложение не существует';
|
||||
} else if (error.message.includes('user rejected')) {
|
||||
errorMessage = 'Транзакция отклонена пользователем';
|
||||
} else if (error.message.includes('insufficient funds')) {
|
||||
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
originalError: error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Исполнить предложение
|
||||
*/
|
||||
const executeProposal = async (contractAddress, proposalId) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
console.log('⚡ Исполняем предложение:', proposalId);
|
||||
|
||||
const tx = await contract.value.executeProposal(proposalId);
|
||||
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
console.log('✅ Предложение исполнено:', receipt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: tx.hash,
|
||||
receipt: receipt
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка исполнения предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
originalError: error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Отменить предложение
|
||||
*/
|
||||
const cancelProposal = async (contractAddress, proposalId, reason) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
console.log('❌ Отменяем предложение:', { proposalId, reason });
|
||||
|
||||
const tx = await contract.value.cancelProposal(proposalId, reason);
|
||||
console.log('📤 Транзакция отправлена:', tx.hash);
|
||||
|
||||
const receipt = await tx.wait();
|
||||
console.log('✅ Предложение отменено:', receipt);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: tx.hash,
|
||||
receipt: receipt
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка отмены предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
originalError: error
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить состояние предложения
|
||||
*/
|
||||
const getProposalState = async (contractAddress, proposalId) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const state = await contract.value.getProposalState(proposalId);
|
||||
|
||||
// 0=Pending, 1=Succeeded, 2=Defeated, 3=Executed, 4=Canceled, 5=ReadyForExecution
|
||||
const stateNames = {
|
||||
0: 'Pending',
|
||||
1: 'Succeeded',
|
||||
2: 'Defeated',
|
||||
3: 'Executed',
|
||||
4: 'Canceled',
|
||||
5: 'ReadyForExecution'
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: state,
|
||||
stateName: stateNames[state] || 'Unknown'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения состояния предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
state: null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Проверить результат предложения
|
||||
*/
|
||||
const checkProposalResult = async (contractAddress, proposalId) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const result = await contract.value.checkProposalResult(proposalId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
passed: result.passed,
|
||||
quorumReached: result.quorumReached
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка проверки результата предложения:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
passed: false,
|
||||
quorumReached: false
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить информацию о DLE
|
||||
*/
|
||||
const getDleInfo = async (contractAddress) => {
|
||||
try {
|
||||
if (!contract.value) {
|
||||
initContract(contractAddress);
|
||||
}
|
||||
|
||||
const info = await contract.value.getDLEInfo();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
name: info.name,
|
||||
symbol: info.symbol,
|
||||
location: info.location,
|
||||
coordinates: info.coordinates,
|
||||
jurisdiction: info.jurisdiction,
|
||||
okvedCodes: info.okvedCodes,
|
||||
kpp: info.kpp,
|
||||
creationTimestamp: info.creationTimestamp,
|
||||
isActive: info.isActive
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка получения информации о DLE:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Вычисляемые свойства
|
||||
const isWalletConnected = computed(() => isConnected.value);
|
||||
const currentUserAddress = computed(() => userAddress.value);
|
||||
const currentChainId = computed(() => chainId.value);
|
||||
|
||||
return {
|
||||
// Состояние
|
||||
isConnected,
|
||||
provider,
|
||||
signer,
|
||||
contract,
|
||||
userAddress,
|
||||
chainId,
|
||||
|
||||
// Вычисляемые свойства
|
||||
isWalletConnected,
|
||||
currentUserAddress,
|
||||
currentChainId,
|
||||
|
||||
// Методы
|
||||
connectWallet,
|
||||
initContract,
|
||||
checkTokenBalance,
|
||||
voteOnProposal,
|
||||
executeProposal,
|
||||
cancelProposal,
|
||||
getProposalState,
|
||||
checkProposalResult,
|
||||
getDleInfo
|
||||
};
|
||||
}
|
||||
207
frontend/src/composables/useProposalValidation.js
Normal file
207
frontend/src/composables/useProposalValidation.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Composable для валидации предложений DLE
|
||||
* Проверяет реальность предложений по хешам транзакций
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export function useProposalValidation() {
|
||||
const validatedProposals = ref([]);
|
||||
const validationErrors = ref([]);
|
||||
const isValidating = ref(false);
|
||||
|
||||
// Проверка формата хеша транзакции
|
||||
const isValidTransactionHash = (hash) => {
|
||||
if (!hash) return false;
|
||||
return /^0x[a-fA-F0-9]{64}$/.test(hash);
|
||||
};
|
||||
|
||||
// Проверка формата адреса
|
||||
const isValidAddress = (address) => {
|
||||
if (!address) return false;
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
};
|
||||
|
||||
// Проверка chainId
|
||||
const isValidChainId = (chainId) => {
|
||||
const validChainIds = [1, 11155111, 17000, 421614, 84532, 8453]; // Mainnet, Sepolia, Holesky, Arbitrum Sepolia, Base Sepolia, Base
|
||||
return validChainIds.includes(Number(chainId));
|
||||
};
|
||||
|
||||
// Валидация предложения
|
||||
const validateProposal = (proposal) => {
|
||||
const errors = [];
|
||||
|
||||
// Проверка обязательных полей
|
||||
if (!proposal.id && proposal.id !== 0) {
|
||||
errors.push('Отсутствует ID предложения');
|
||||
}
|
||||
|
||||
if (!proposal.description || proposal.description.trim() === '') {
|
||||
errors.push('Отсутствует описание предложения');
|
||||
}
|
||||
|
||||
if (!proposal.transactionHash) {
|
||||
errors.push('Отсутствует хеш транзакции');
|
||||
} else if (!isValidTransactionHash(proposal.transactionHash)) {
|
||||
errors.push('Неверный формат хеша транзакции');
|
||||
}
|
||||
|
||||
if (!proposal.initiator) {
|
||||
errors.push('Отсутствует инициатор предложения');
|
||||
} else if (!isValidAddress(proposal.initiator)) {
|
||||
errors.push('Неверный формат адреса инициатора');
|
||||
}
|
||||
|
||||
if (!proposal.chainId) {
|
||||
errors.push('Отсутствует chainId');
|
||||
} else if (!isValidChainId(proposal.chainId)) {
|
||||
errors.push('Неподдерживаемый chainId');
|
||||
}
|
||||
|
||||
if (proposal.state === undefined || proposal.state === null) {
|
||||
errors.push('Отсутствует статус предложения');
|
||||
}
|
||||
|
||||
// Проверка числовых значений
|
||||
if (typeof proposal.forVotes !== 'number' || proposal.forVotes < 0) {
|
||||
errors.push('Неверное значение голосов "за"');
|
||||
}
|
||||
|
||||
if (typeof proposal.againstVotes !== 'number' || proposal.againstVotes < 0) {
|
||||
errors.push('Неверное значение голосов "против"');
|
||||
}
|
||||
|
||||
if (typeof proposal.quorumRequired !== 'number' || proposal.quorumRequired < 0) {
|
||||
errors.push('Неверное значение требуемого кворума');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
};
|
||||
|
||||
// Валидация массива предложений
|
||||
const validateProposals = (proposals) => {
|
||||
isValidating.value = true;
|
||||
validationErrors.value = [];
|
||||
validatedProposals.value = [];
|
||||
|
||||
const validProposals = [];
|
||||
const allErrors = [];
|
||||
|
||||
proposals.forEach((proposal, index) => {
|
||||
const validation = validateProposal(proposal);
|
||||
|
||||
if (validation.isValid) {
|
||||
validProposals.push(proposal);
|
||||
} else {
|
||||
allErrors.push({
|
||||
proposalIndex: index,
|
||||
proposalId: proposal.id,
|
||||
errors: validation.errors
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
validatedProposals.value = validProposals;
|
||||
validationErrors.value = allErrors;
|
||||
isValidating.value = false;
|
||||
|
||||
console.log(`[Proposal Validation] Проверено предложений: ${proposals.length}`);
|
||||
console.log(`[Proposal Validation] Валидных: ${validProposals.length}`);
|
||||
console.log(`[Proposal Validation] С ошибками: ${allErrors.length}`);
|
||||
|
||||
return {
|
||||
validProposals,
|
||||
errors: allErrors,
|
||||
totalCount: proposals.length,
|
||||
validCount: validProposals.length,
|
||||
errorCount: allErrors.length
|
||||
};
|
||||
};
|
||||
|
||||
// Получение статистики валидации
|
||||
const validationStats = computed(() => {
|
||||
const total = validatedProposals.value.length + validationErrors.value.length;
|
||||
const valid = validatedProposals.value.length;
|
||||
const invalid = validationErrors.value.length;
|
||||
|
||||
return {
|
||||
total,
|
||||
valid,
|
||||
invalid,
|
||||
validPercentage: total > 0 ? Math.round((valid / total) * 100) : 0,
|
||||
invalidPercentage: total > 0 ? Math.round((invalid / total) * 100) : 0
|
||||
};
|
||||
});
|
||||
|
||||
// Проверка, является ли предложение реальным (на основе хеша транзакции)
|
||||
const isRealProposal = (proposal) => {
|
||||
if (!proposal.transactionHash) return false;
|
||||
|
||||
// Проверяем, что хеш имеет правильный формат
|
||||
if (!isValidTransactionHash(proposal.transactionHash)) return false;
|
||||
|
||||
// Проверяем, что это не тестовые/фейковые хеши
|
||||
const fakeHashes = [
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||
];
|
||||
|
||||
if (fakeHashes.includes(proposal.transactionHash.toLowerCase())) return false;
|
||||
|
||||
// Проверяем, что хеш не начинается с нулей (подозрительно)
|
||||
if (proposal.transactionHash.startsWith('0x0000')) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Фильтрация только реальных предложений
|
||||
const filterRealProposals = (proposals) => {
|
||||
return proposals.filter(proposal => isRealProposal(proposal));
|
||||
};
|
||||
|
||||
// Фильтрация активных предложений (исключает выполненные и отмененные)
|
||||
const filterActiveProposals = (proposals) => {
|
||||
return proposals.filter(proposal => {
|
||||
// Исключаем выполненные и отмененные предложения
|
||||
if (proposal.executed || proposal.canceled) {
|
||||
console.log(`🚫 [FILTER] Исключаем предложение ${proposal.id}: executed=${proposal.executed}, canceled=${proposal.canceled}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Исключаем предложения с истекшим deadline
|
||||
if (proposal.deadline) {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (currentTime > proposal.deadline) {
|
||||
console.log(`⏰ [FILTER] Исключаем предложение ${proposal.id}: deadline истек`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
// Данные
|
||||
validatedProposals,
|
||||
validationErrors,
|
||||
isValidating,
|
||||
validationStats,
|
||||
|
||||
// Методы
|
||||
validateProposal,
|
||||
validateProposals,
|
||||
isRealProposal,
|
||||
filterRealProposals,
|
||||
filterActiveProposals,
|
||||
|
||||
// Вспомогательные функции
|
||||
isValidTransactionHash,
|
||||
isValidAddress,
|
||||
isValidChainId
|
||||
};
|
||||
}
|
||||
534
frontend/src/composables/useProposals.js
Normal file
534
frontend/src/composables/useProposals.js
Normal file
@@ -0,0 +1,534 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { getProposals } from '@/services/proposalsService';
|
||||
import { ethers } from 'ethers';
|
||||
import { useProposalValidation } from './useProposalValidation';
|
||||
import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract';
|
||||
|
||||
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
|
||||
// Функция checkTokenBalance перенесена в useDleContract.js
|
||||
|
||||
// Функция sendTransactionToWallet удалена - теперь используется прямое взаимодействие с контрактом
|
||||
|
||||
export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
||||
const proposals = ref([]);
|
||||
const filteredProposals = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const isVoting = ref(false);
|
||||
const isExecuting = ref(false);
|
||||
const isCancelling = ref(false);
|
||||
const statusFilter = ref('');
|
||||
const searchQuery = ref('');
|
||||
|
||||
// Используем готовые функции из utils/dle-contract.js
|
||||
|
||||
// Инициализируем валидацию
|
||||
const {
|
||||
validateProposals,
|
||||
filterRealProposals,
|
||||
filterActiveProposals,
|
||||
validationStats,
|
||||
isValidating
|
||||
} = useProposalValidation();
|
||||
|
||||
const loadProposals = async () => {
|
||||
if (!dleAddress.value) {
|
||||
console.warn('Адрес DLE не найден');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const response = await getProposals(dleAddress.value);
|
||||
|
||||
if (response.success) {
|
||||
const rawProposals = response.data.proposals || [];
|
||||
|
||||
console.log(`[Proposals] Загружено предложений: ${rawProposals.length}`);
|
||||
console.log(`[Proposals] Полные данные из блокчейна:`, rawProposals);
|
||||
|
||||
// Детальная информация о каждом предложении
|
||||
rawProposals.forEach((proposal, index) => {
|
||||
console.log(`[Proposals] Предложение ${index}:`, {
|
||||
id: proposal.id,
|
||||
description: proposal.description,
|
||||
state: proposal.state,
|
||||
forVotes: proposal.forVotes,
|
||||
againstVotes: proposal.againstVotes,
|
||||
quorumRequired: proposal.quorumRequired,
|
||||
quorumReached: proposal.quorumReached,
|
||||
executed: proposal.executed,
|
||||
canceled: proposal.canceled,
|
||||
initiator: proposal.initiator,
|
||||
chainId: proposal.chainId,
|
||||
transactionHash: proposal.transactionHash
|
||||
});
|
||||
});
|
||||
|
||||
// Применяем валидацию предложений
|
||||
const validationResult = validateProposals(rawProposals);
|
||||
|
||||
// Фильтруем только реальные предложения
|
||||
const realProposals = filterRealProposals(validationResult.validProposals);
|
||||
|
||||
// Фильтруем только активные предложения (исключаем выполненные и отмененные)
|
||||
const activeProposals = filterActiveProposals(realProposals);
|
||||
|
||||
console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`);
|
||||
console.log(`[Proposals] Реальных предложений: ${realProposals.length}`);
|
||||
console.log(`[Proposals] Активных предложений: ${activeProposals.length}`);
|
||||
|
||||
if (validationResult.errorCount > 0) {
|
||||
console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`);
|
||||
}
|
||||
|
||||
proposals.value = activeProposals;
|
||||
filterProposals();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки предложений:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filterProposals = () => {
|
||||
if (!proposals.value || proposals.value.length === 0) {
|
||||
filteredProposals.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
let filtered = [...proposals.value];
|
||||
|
||||
if (statusFilter.value) {
|
||||
filtered = filtered.filter(proposal => {
|
||||
switch (statusFilter.value) {
|
||||
case 'active': return proposal.state === 0; // Pending
|
||||
case 'succeeded': return proposal.state === 1; // Succeeded
|
||||
case 'defeated': return proposal.state === 2; // Defeated
|
||||
case 'executed': return proposal.state === 3; // Executed
|
||||
case 'cancelled': return proposal.state === 4; // Canceled
|
||||
case 'ready': return proposal.state === 5; // ReadyForExecution
|
||||
default: return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
filtered = filtered.filter(proposal =>
|
||||
proposal.description.toLowerCase().includes(query) ||
|
||||
proposal.initiator.toLowerCase().includes(query) ||
|
||||
proposal.uniqueId.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
filteredProposals.value = filtered;
|
||||
};
|
||||
|
||||
const voteOnProposal = async (proposalId, support) => {
|
||||
try {
|
||||
console.log('🚀 [VOTE] Начинаем голосование через DLE контракт:', { proposalId, support, dleAddress: dleAddress.value, userAddress: userAddress.value });
|
||||
isVoting.value = true;
|
||||
|
||||
// Проверяем наличие MetaMask
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||
}
|
||||
|
||||
// Проверяем состояние предложения
|
||||
console.log('🔍 [DEBUG] Проверяем состояние предложения...');
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (!proposal) {
|
||||
throw new Error('Предложение не найдено');
|
||||
}
|
||||
|
||||
console.log('📊 [DEBUG] Данные предложения:', {
|
||||
id: proposal.id,
|
||||
state: proposal.state,
|
||||
deadline: proposal.deadline,
|
||||
forVotes: proposal.forVotes,
|
||||
againstVotes: proposal.againstVotes,
|
||||
executed: proposal.executed,
|
||||
canceled: proposal.canceled
|
||||
});
|
||||
|
||||
// Проверяем, что предложение активно (Pending)
|
||||
if (proposal.state !== 0) {
|
||||
const statusText = getProposalStatusText(proposal.state);
|
||||
throw new Error(`Предложение не активно (статус: ${statusText}). Голосование возможно только для активных предложений.`);
|
||||
}
|
||||
|
||||
// Проверяем, что предложение не выполнено и не отменено
|
||||
if (proposal.executed) {
|
||||
throw new Error('Предложение уже выполнено. Голосование невозможно.');
|
||||
}
|
||||
|
||||
if (proposal.canceled) {
|
||||
throw new Error('Предложение отменено. Голосование невозможно.');
|
||||
}
|
||||
|
||||
// Проверяем deadline
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (proposal.deadline && currentTime > proposal.deadline) {
|
||||
throw new Error('Время голосования истекло. Голосование невозможно.');
|
||||
}
|
||||
|
||||
// Проверяем баланс токенов пользователя
|
||||
console.log('💰 [DEBUG] Проверяем баланс токенов...');
|
||||
try {
|
||||
const balanceCheck = await checkTokenBalance(dleAddress.value, userAddress.value);
|
||||
console.log('💰 [DEBUG] Баланс токенов:', balanceCheck);
|
||||
|
||||
if (!balanceCheck.hasTokens) {
|
||||
throw new Error('У вас нет токенов для голосования. Необходимо иметь токены DLE для участия в голосовании.');
|
||||
}
|
||||
} catch (balanceError) {
|
||||
console.warn('⚠️ [DEBUG] Ошибка проверки баланса (продолжаем):', balanceError.message);
|
||||
// Не останавливаем голосование, если не удалось проверить баланс
|
||||
}
|
||||
|
||||
// Проверяем сеть кошелька
|
||||
console.log('🌐 [DEBUG] Проверяем сеть кошелька...');
|
||||
try {
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
console.log('🌐 [DEBUG] Текущая сеть:', chainId);
|
||||
console.log('🌐 [DEBUG] Сеть предложения:', proposal.chainId);
|
||||
|
||||
if (chainId !== proposal.chainId) {
|
||||
throw new Error(`Неправильная сеть! Текущая сеть: ${chainId}, требуется: ${proposal.chainId}`);
|
||||
}
|
||||
} catch (networkError) {
|
||||
console.warn('⚠️ [DEBUG] Ошибка проверки сети (продолжаем):', networkError.message);
|
||||
}
|
||||
|
||||
// Голосуем через готовую функцию из utils/dle-contract.js
|
||||
console.log('🗳️ Отправляем голосование через смарт-контракт...');
|
||||
const result = await voteForProposal(dleAddress.value, proposalId, support);
|
||||
|
||||
console.log('✅ Голосование успешно отправлено:', result.txHash);
|
||||
alert(`Голосование успешно отправлено! Хеш транзакции: ${result.txHash}`);
|
||||
|
||||
// Принудительно обновляем данные предложения
|
||||
console.log('🔄 [VOTE] Обновляем данные после голосования...');
|
||||
await loadProposals();
|
||||
|
||||
// Дополнительная задержка для подтверждения в блокчейне
|
||||
setTimeout(async () => {
|
||||
console.log('🔄 [VOTE] Повторное обновление через 3 секунды...');
|
||||
await loadProposals();
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка голосования:', error);
|
||||
|
||||
// Улучшенная обработка ошибок
|
||||
let errorMessage = error.message;
|
||||
|
||||
if (error.message.includes('execution reverted')) {
|
||||
if (error.data === '0xe7005635') {
|
||||
errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' +
|
||||
'• Вы уже голосовали за это предложение\n' +
|
||||
'• У вас нет токенов для голосования\n' +
|
||||
'• Предложение не активно\n' +
|
||||
'• Время голосования истекло';
|
||||
} else if (error.data === '0xc7567e07') {
|
||||
errorMessage = 'Голосование отклонено смарт-контрактом. Возможные причины:\n' +
|
||||
'• Вы уже голосовали за это предложение\n' +
|
||||
'• У вас нет токенов для голосования\n' +
|
||||
'• Предложение не активно\n' +
|
||||
'• Время голосования истекло\n' +
|
||||
'• Неправильная сеть для голосования';
|
||||
} else {
|
||||
errorMessage = `Транзакция отклонена смарт-контрактом (код: ${error.data}). Проверьте условия голосования.`;
|
||||
}
|
||||
} else if (error.message.includes('user rejected')) {
|
||||
errorMessage = 'Транзакция отклонена пользователем';
|
||||
} else if (error.message.includes('insufficient funds')) {
|
||||
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||
}
|
||||
|
||||
alert('Ошибка при голосовании: ' + errorMessage);
|
||||
} finally {
|
||||
isVoting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const executeProposal = async (proposalId) => {
|
||||
try {
|
||||
console.log('⚡ [EXECUTE] Исполняем предложение через DLE контракт:', { proposalId, dleAddress: dleAddress.value });
|
||||
isExecuting.value = true;
|
||||
|
||||
// Проверяем состояние предложения перед выполнением
|
||||
console.log('🔍 [DEBUG] Проверяем состояние предложения для выполнения...');
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (!proposal) {
|
||||
throw new Error('Предложение не найдено');
|
||||
}
|
||||
|
||||
console.log('📊 [DEBUG] Данные предложения для выполнения:', {
|
||||
id: proposal.id,
|
||||
state: proposal.state,
|
||||
executed: proposal.executed,
|
||||
canceled: proposal.canceled,
|
||||
quorumReached: proposal.quorumReached
|
||||
});
|
||||
|
||||
// Проверяем, что предложение можно выполнить
|
||||
if (proposal.executed) {
|
||||
throw new Error('Предложение уже выполнено. Повторное выполнение невозможно.');
|
||||
}
|
||||
|
||||
if (proposal.canceled) {
|
||||
throw new Error('Предложение отменено. Выполнение невозможно.');
|
||||
}
|
||||
|
||||
// Проверяем, что предложение готово к выполнению
|
||||
if (proposal.state !== 5) {
|
||||
const statusText = getProposalStatusText(proposal.state);
|
||||
throw new Error(`Предложение не готово к выполнению (статус: ${statusText}). Выполнение возможно только для предложений в статусе "Готово к выполнению".`);
|
||||
}
|
||||
|
||||
// Исполняем предложение через готовую функцию из utils/dle-contract.js
|
||||
const result = await executeProposalUtil(dleAddress.value, proposalId);
|
||||
|
||||
console.log('✅ Предложение успешно исполнено:', result.txHash);
|
||||
alert(`Предложение успешно исполнено! Хеш транзакции: ${result.txHash}`);
|
||||
|
||||
// Принудительно обновляем состояние предложения в UI
|
||||
updateProposalState(proposalId, {
|
||||
executed: true,
|
||||
state: 1, // Выполнено
|
||||
canceled: false
|
||||
});
|
||||
|
||||
await loadProposals(); // Перезагружаем данные
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка выполнения предложения:', error);
|
||||
|
||||
// Улучшенная обработка ошибок
|
||||
let errorMessage = error.message;
|
||||
|
||||
if (error.message.includes('execution reverted')) {
|
||||
errorMessage = 'Выполнение отклонено смарт-контрактом. Возможные причины:\n' +
|
||||
'• Предложение уже выполнено\n' +
|
||||
'• Предложение отменено\n' +
|
||||
'• Кворум не достигнут\n' +
|
||||
'• Предложение не активно';
|
||||
} else if (error.message.includes('user rejected')) {
|
||||
errorMessage = 'Транзакция отклонена пользователем';
|
||||
} else if (error.message.includes('insufficient funds')) {
|
||||
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||
}
|
||||
|
||||
alert('Ошибка при исполнении предложения: ' + errorMessage);
|
||||
} finally {
|
||||
isExecuting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const cancelProposal = async (proposalId, reason = 'Отменено пользователем') => {
|
||||
try {
|
||||
console.log('❌ [CANCEL] Отменяем предложение через DLE контракт:', { proposalId, reason, dleAddress: dleAddress.value });
|
||||
isCancelling.value = true;
|
||||
|
||||
// Проверяем состояние предложения перед отменой
|
||||
console.log('🔍 [DEBUG] Проверяем состояние предложения для отмены...');
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (!proposal) {
|
||||
throw new Error('Предложение не найдено');
|
||||
}
|
||||
|
||||
console.log('📊 [DEBUG] Данные предложения для отмены:', {
|
||||
id: proposal.id,
|
||||
state: proposal.state,
|
||||
executed: proposal.executed,
|
||||
canceled: proposal.canceled,
|
||||
deadline: proposal.deadline
|
||||
});
|
||||
|
||||
// Проверяем, что предложение можно отменить
|
||||
if (proposal.executed) {
|
||||
throw new Error('Предложение уже выполнено. Отмена невозможна.');
|
||||
}
|
||||
|
||||
if (proposal.canceled) {
|
||||
throw new Error('Предложение уже отменено. Повторная отмена невозможна.');
|
||||
}
|
||||
|
||||
// Проверяем, что предложение активно (Pending)
|
||||
if (proposal.state !== 0) {
|
||||
const statusText = getProposalStatusText(proposal.state);
|
||||
throw new Error(`Предложение не активно (статус: ${statusText}). Отмена возможна только для активных предложений.`);
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь является инициатором
|
||||
if (proposal.initiator !== userAddress.value) {
|
||||
throw new Error('Только инициатор предложения может его отменить.');
|
||||
}
|
||||
|
||||
// Проверяем deadline (нужен запас 15 минут)
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (proposal.deadline) {
|
||||
const timeRemaining = proposal.deadline - currentTime;
|
||||
if (timeRemaining <= 900) { // 15 минут запас
|
||||
throw new Error('Время для отмены истекло. Отмена возможна только за 15 минут до окончания голосования.');
|
||||
}
|
||||
}
|
||||
|
||||
// Отменяем предложение через готовую функцию из utils/dle-contract.js
|
||||
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
|
||||
|
||||
console.log('✅ Предложение успешно отменено:', result.txHash);
|
||||
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
|
||||
|
||||
// Принудительно обновляем состояние предложения в UI
|
||||
updateProposalState(proposalId, {
|
||||
canceled: true,
|
||||
state: 2, // Отменено
|
||||
executed: false
|
||||
});
|
||||
|
||||
await loadProposals(); // Перезагружаем данные
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка отмены предложения:', error);
|
||||
|
||||
// Улучшенная обработка ошибок
|
||||
let errorMessage = error.message;
|
||||
|
||||
if (error.message.includes('execution reverted')) {
|
||||
errorMessage = 'Отмена отклонена смарт-контрактом. Возможные причины:\n' +
|
||||
'• Предложение уже отменено\n' +
|
||||
'• Предложение уже выполнено\n' +
|
||||
'• Предложение не активно\n' +
|
||||
'• Недостаточно прав для отмены';
|
||||
} else if (error.message.includes('user rejected')) {
|
||||
errorMessage = 'Транзакция отклонена пользователем';
|
||||
} else if (error.message.includes('insufficient funds')) {
|
||||
errorMessage = 'Недостаточно средств для оплаты газа';
|
||||
}
|
||||
|
||||
alert('Ошибка при отмене предложения: ' + errorMessage);
|
||||
} finally {
|
||||
isCancelling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getProposalStatusClass = (state) => {
|
||||
switch (state) {
|
||||
case 0: return 'status-active'; // Pending
|
||||
case 1: return 'status-succeeded'; // Succeeded
|
||||
case 2: return 'status-defeated'; // Defeated
|
||||
case 3: return 'status-executed'; // Executed
|
||||
case 4: return 'status-cancelled'; // Canceled
|
||||
case 5: return 'status-ready'; // ReadyForExecution
|
||||
default: return 'status-active';
|
||||
}
|
||||
};
|
||||
|
||||
const getProposalStatusText = (state) => {
|
||||
switch (state) {
|
||||
case 0: return 'Активное';
|
||||
case 1: return 'Успешное';
|
||||
case 2: return 'Отклоненное';
|
||||
case 3: return 'Выполнено';
|
||||
case 4: return 'Отменено';
|
||||
case 5: return 'Готово к выполнению';
|
||||
default: return 'Неизвестно';
|
||||
}
|
||||
};
|
||||
|
||||
const getQuorumPercentage = (proposal) => {
|
||||
// Получаем реальные данные из предложения
|
||||
const forVotes = Number(proposal.forVotes || 0);
|
||||
const againstVotes = Number(proposal.againstVotes || 0);
|
||||
const totalVotes = forVotes + againstVotes;
|
||||
|
||||
// Используем реальный totalSupply из предложения или fallback
|
||||
const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE
|
||||
|
||||
console.log(`📊 [QUORUM] Предложение ${proposal.id}:`, {
|
||||
forVotes: forVotes,
|
||||
againstVotes: againstVotes,
|
||||
totalVotes: totalVotes,
|
||||
totalSupply: totalSupply,
|
||||
forVotesFormatted: `${(forVotes / 1e+18).toFixed(2)} DLE`,
|
||||
againstVotesFormatted: `${(againstVotes / 1e+18).toFixed(2)} DLE`,
|
||||
totalVotesFormatted: `${(totalVotes / 1e+18).toFixed(2)} DLE`,
|
||||
totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE`
|
||||
});
|
||||
|
||||
const percentage = totalSupply > 0 ? (totalVotes / totalSupply) * 100 : 0;
|
||||
return percentage.toFixed(2);
|
||||
};
|
||||
|
||||
const getRequiredQuorumPercentage = (proposal) => {
|
||||
// Получаем требуемый кворум из предложения
|
||||
const requiredQuorum = Number(proposal.quorumRequired || 0);
|
||||
|
||||
// Используем реальный totalSupply из предложения или fallback
|
||||
const totalSupply = Number(proposal.totalSupply || 3e+24); // Fallback к 3M DLE
|
||||
|
||||
console.log(`📊 [REQUIRED QUORUM] Предложение ${proposal.id}:`, {
|
||||
requiredQuorum: requiredQuorum,
|
||||
totalSupply: totalSupply,
|
||||
requiredQuorumFormatted: `${(requiredQuorum / 1e+18).toFixed(2)} DLE`,
|
||||
totalSupplyFormatted: `${(totalSupply / 1e+18).toFixed(2)} DLE`
|
||||
});
|
||||
|
||||
const percentage = totalSupply > 0 ? (requiredQuorum / totalSupply) * 100 : 0;
|
||||
return percentage.toFixed(2);
|
||||
};
|
||||
|
||||
const canVote = (proposal) => {
|
||||
return proposal.state === 0; // Pending - только активные предложения
|
||||
};
|
||||
|
||||
const canExecute = (proposal) => {
|
||||
return proposal.state === 5; // ReadyForExecution - готово к выполнению
|
||||
};
|
||||
|
||||
const canCancel = (proposal) => {
|
||||
// Можно отменить только активные предложения (Pending)
|
||||
return proposal.state === 0 &&
|
||||
!proposal.executed &&
|
||||
!proposal.canceled;
|
||||
};
|
||||
|
||||
// Принудительное обновление состояния предложения в UI
|
||||
const updateProposalState = (proposalId, updates) => {
|
||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
||||
if (proposal) {
|
||||
Object.assign(proposal, updates);
|
||||
console.log(`🔄 [UI] Обновлено состояние предложения ${proposalId}:`, updates);
|
||||
|
||||
// Принудительно обновляем фильтрацию
|
||||
filterProposals();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
proposals,
|
||||
filteredProposals,
|
||||
isLoading,
|
||||
isVoting,
|
||||
isExecuting,
|
||||
isCancelling,
|
||||
statusFilter,
|
||||
searchQuery,
|
||||
loadProposals,
|
||||
filterProposals,
|
||||
voteOnProposal,
|
||||
executeProposal,
|
||||
cancelProposal,
|
||||
getProposalStatusClass,
|
||||
getProposalStatusText,
|
||||
getQuorumPercentage,
|
||||
getRequiredQuorumPercentage,
|
||||
canVote,
|
||||
canExecute,
|
||||
canCancel,
|
||||
updateProposalState,
|
||||
// Валидация
|
||||
validationStats,
|
||||
isValidating
|
||||
};
|
||||
}
|
||||
@@ -228,14 +228,9 @@ const routes = [
|
||||
component: () => import('../views/smartcontracts/CreateProposalView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/tokens',
|
||||
name: 'management-tokens',
|
||||
component: () => import('../views/smartcontracts/TokensView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/quorum',
|
||||
name: 'management-quorum',
|
||||
component: () => import('../views/smartcontracts/QuorumView.vue')
|
||||
path: '/management/add-module',
|
||||
name: 'management-add-module',
|
||||
component: () => import('../views/smartcontracts/AddModuleFormView.vue')
|
||||
},
|
||||
{
|
||||
path: '/management/modules',
|
||||
|
||||
@@ -57,24 +57,24 @@ export default {
|
||||
},
|
||||
// --- Работа с тегами пользователя ---
|
||||
async addTagsToContact(contactId, tagIds) {
|
||||
// PATCH /api/tags/user/:id { tags: [...] }
|
||||
// PATCH /tags/user/:id { tags: [...] }
|
||||
const res = await api.patch(`/tags/user/${contactId}`, { tags: tagIds });
|
||||
return res.data;
|
||||
},
|
||||
async getContactTags(contactId) {
|
||||
// GET /api/tags/user/:id
|
||||
// GET /tags/user/:id
|
||||
const res = await api.get(`/tags/user/${contactId}`);
|
||||
return res.data.tags || [];
|
||||
},
|
||||
async removeTagFromContact(contactId, tagId) {
|
||||
// DELETE /api/tags/user/:id/tag/:tagId
|
||||
// DELETE /tags/user/:id/tag/:tagId
|
||||
const res = await api.delete(`/tags/user/${contactId}/tag/${tagId}`);
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
|
||||
export async function getContacts() {
|
||||
const res = await fetch('/api/users');
|
||||
const res = await fetch('/users');
|
||||
const data = await res.json();
|
||||
if (data && data.success) {
|
||||
return data.contacts;
|
||||
|
||||
@@ -13,6 +13,23 @@
|
||||
// Сервис для работы с модулями DLE
|
||||
import api from '@/api/axios';
|
||||
|
||||
/**
|
||||
* Получить deploymentId по адресу DLE
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
* @returns {Promise<Object>} - Результат с deploymentId
|
||||
*/
|
||||
export const getDeploymentId = async (dleAddress) => {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/get-deployment-id', {
|
||||
dleAddress
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении deploymentId:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Создает предложение о добавлении модуля
|
||||
* @param {string} dleAddress - Адрес DLE
|
||||
|
||||
183
frontend/src/services/multichainExecutionService.js
Normal file
183
frontend/src/services/multichainExecutionService.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import api from '@/api/axios';
|
||||
|
||||
/**
|
||||
* Получить информацию о мультиконтрактном предложении
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {number} governanceChainId - ID сети голосования
|
||||
* @returns {Promise<Object>} - Информация о предложении
|
||||
*/
|
||||
export async function getProposalMultichainInfo(dleAddress, proposalId, governanceChainId) {
|
||||
try {
|
||||
const response = await api.post('/dle-multichain-execution/get-proposal-multichain-info', {
|
||||
dleAddress,
|
||||
proposalId,
|
||||
governanceChainId
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.error || 'Не удалось получить информацию о мультиконтрактном предложении');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения информации о мультиконтрактном предложении:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исполнить предложение во всех целевых сетях
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {string} deploymentId - ID деплоя
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export async function executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress) {
|
||||
try {
|
||||
const response = await api.post('/dle-multichain-execution/execute-in-all-target-chains', {
|
||||
dleAddress,
|
||||
proposalId,
|
||||
deploymentId,
|
||||
userAddress
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.error || 'Не удалось исполнить предложение во всех целевых сетях');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка исполнения во всех целевых сетях:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исполнить предложение в конкретной целевой сети
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {number} targetChainId - ID целевой сети
|
||||
* @param {string} deploymentId - ID деплоя
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export async function executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress) {
|
||||
try {
|
||||
const response = await api.post('/dle-multichain-execution/execute-in-target-chain', {
|
||||
dleAddress,
|
||||
proposalId,
|
||||
targetChainId,
|
||||
deploymentId,
|
||||
userAddress
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.error || 'Не удалось исполнить предложение в целевой сети');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка исполнения в целевой сети:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить deploymentId по адресу DLE
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @returns {Promise<string>} - ID деплоя
|
||||
*/
|
||||
export async function getDeploymentId(dleAddress) {
|
||||
try {
|
||||
const response = await api.post('/dle-modules/get-deployment-id', {
|
||||
dleAddress
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data.deploymentId;
|
||||
} else {
|
||||
throw new Error(response.data.error || 'Не удалось получить ID деплоя');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения ID деплоя:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли предложение мультиконтрактным
|
||||
* @param {Object} proposal - Предложение
|
||||
* @returns {boolean} - Является ли мультиконтрактным
|
||||
*/
|
||||
export function isMultichainProposal(proposal) {
|
||||
return proposal.targetChains && proposal.targetChains.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить название сети по ID
|
||||
* @param {number} chainId - ID сети
|
||||
* @returns {string} - Название сети
|
||||
*/
|
||||
export function getChainName(chainId) {
|
||||
const chainNames = {
|
||||
1: 'Ethereum Mainnet',
|
||||
11155111: 'Sepolia',
|
||||
17000: 'Holesky',
|
||||
421614: 'Arbitrum Sepolia',
|
||||
84532: 'Base Sepolia',
|
||||
137: 'Polygon',
|
||||
80001: 'Polygon Mumbai',
|
||||
56: 'BSC',
|
||||
97: 'BSC Testnet'
|
||||
};
|
||||
|
||||
return chainNames[chainId] || `Chain ${chainId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматировать результат исполнения
|
||||
* @param {Object} result - Результат исполнения
|
||||
* @returns {string} - Отформатированный результат
|
||||
*/
|
||||
export function formatExecutionResult(result) {
|
||||
const { summary, executionResults } = result;
|
||||
|
||||
if (summary.successful === summary.total) {
|
||||
return `✅ Успешно исполнено во всех ${summary.total} сетях`;
|
||||
} else if (summary.successful > 0) {
|
||||
return `⚠️ Частично исполнено: ${summary.successful}/${summary.total} сетей`;
|
||||
} else {
|
||||
return `❌ Не удалось исполнить ни в одной сети`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить детали ошибок исполнения
|
||||
* @param {Object} result - Результат исполнения
|
||||
* @returns {Array} - Массив ошибок
|
||||
*/
|
||||
export function getExecutionErrors(result) {
|
||||
return result.executionResults
|
||||
.filter(r => !r.success)
|
||||
.map(r => ({
|
||||
chainId: r.chainId,
|
||||
chainName: getChainName(r.chainId),
|
||||
error: r.error
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,15 @@ import axios from 'axios';
|
||||
*/
|
||||
export const getProposals = async (dleAddress) => {
|
||||
try {
|
||||
console.log(`🌐 [API] Запрашиваем предложения для DLE: ${dleAddress}`);
|
||||
const response = await axios.post('/dle-proposals/get-proposals', { dleAddress });
|
||||
|
||||
console.log(`🌐 [API] Ответ от backend:`, {
|
||||
success: response.data.success,
|
||||
proposalsCount: response.data.data?.proposals?.length || 0,
|
||||
fullResponse: response.data
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении предложений:', error);
|
||||
@@ -73,13 +81,21 @@ export const createProposal = async (dleAddress, proposalData) => {
|
||||
* @param {boolean} support - Поддержка предложения
|
||||
* @returns {Promise<Object>} - Результат голосования
|
||||
*/
|
||||
export const voteOnProposal = async (dleAddress, proposalId, support) => {
|
||||
export const voteOnProposal = async (dleAddress, proposalId, support, userAddress) => {
|
||||
try {
|
||||
const response = await axios.post('/dle-proposals/vote-proposal', {
|
||||
const requestData = {
|
||||
dleAddress,
|
||||
proposalId,
|
||||
support
|
||||
});
|
||||
support,
|
||||
voterAddress: userAddress
|
||||
};
|
||||
|
||||
console.log('📤 [SERVICE] Отправляем запрос на голосование:', requestData);
|
||||
|
||||
const response = await axios.post('/dle-proposals/vote-proposal', requestData);
|
||||
|
||||
console.log('📥 [SERVICE] Ответ от бэкенда:', response.data);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при голосовании:', error);
|
||||
|
||||
@@ -43,6 +43,10 @@ export async function connectWithWallet() {
|
||||
const nonceResponse = await axios.get(`/auth/nonce?address=${address}`);
|
||||
const nonce = nonceResponse.data.nonce;
|
||||
// console.log('Got nonce:', nonce);
|
||||
|
||||
if (!nonce) {
|
||||
throw new Error('Не удалось получить nonce с сервера');
|
||||
}
|
||||
|
||||
// Создаем сообщение для подписи
|
||||
const domain = window.location.host;
|
||||
@@ -73,7 +77,7 @@ export async function connectWithWallet() {
|
||||
// chainId: 1,
|
||||
// nonce,
|
||||
// issuedAt,
|
||||
// resources: [`${origin}/api/auth/verify`],
|
||||
// resources: [`${origin}/auth/verify`],
|
||||
// });
|
||||
|
||||
// Запрашиваем подпись
|
||||
|
||||
106
frontend/src/utils/dle-abi.js
Normal file
106
frontend/src/utils/dle-abi.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* ABI для DLE смарт-контракта
|
||||
* АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
|
||||
* Для обновления запустите: node backend/scripts/generate-abi.js
|
||||
*
|
||||
* Последнее обновление: 2025-09-29T18:16:32.027Z
|
||||
*/
|
||||
|
||||
export const DLE_ABI = [
|
||||
"function CLOCK_MODE() returns (string)",
|
||||
"function DOMAIN_SEPARATOR() returns (bytes32)",
|
||||
"function activeModules(bytes32 ) returns (bool)",
|
||||
"function allProposalIds(uint256 ) returns (uint256)",
|
||||
"function allowance(address owner, address spender) returns (uint256)",
|
||||
"function approve(address , uint256 ) returns (bool)",
|
||||
"function balanceOf(address account) returns (uint256)",
|
||||
"function cancelProposal(uint256 _proposalId, string reason)",
|
||||
"function checkProposalResult(uint256 _proposalId) returns (bool, bool)",
|
||||
"function checkpoints(address account, uint32 pos) returns (tuple)",
|
||||
"function clock() returns (uint48)",
|
||||
"function createAddModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) returns (uint256)",
|
||||
"function createProposal(string _description, uint256 _duration, bytes _operation, uint256 _governanceChainId, uint256[] _targetChains, uint256 ) returns (uint256)",
|
||||
"function createRemoveModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) returns (uint256)",
|
||||
"function decimals() returns (uint8)",
|
||||
"function delegate(address delegatee)",
|
||||
"function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)",
|
||||
"function delegates(address account) returns (address)",
|
||||
"function dleInfo() returns (string, string, string, string, uint256, uint256, uint256, bool)",
|
||||
"function eip712Domain() returns (bytes1, string, string, uint256, address, bytes32, uint256[])",
|
||||
"function executeProposal(uint256 _proposalId)",
|
||||
"function executeProposalBySignatures(uint256 _proposalId, address[] signers, bytes[] signatures)",
|
||||
"function getCurrentChainId() returns (uint256)",
|
||||
"function getDLEInfo() returns (tuple)",
|
||||
"function getModuleAddress(bytes32 _moduleId) returns (address)",
|
||||
"function getMultichainAddresses() returns (uint256[], address[])",
|
||||
"function getMultichainInfo() returns (uint256[], uint256)",
|
||||
"function getMultichainMetadata() returns (string)",
|
||||
"function getPastTotalSupply(uint256 timepoint) returns (uint256)",
|
||||
"function getPastVotes(address account, uint256 timepoint) returns (uint256)",
|
||||
"function getProposalState(uint256 _proposalId) returns (uint8)",
|
||||
"function getProposalSummary(uint256 _proposalId) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, uint256, uint256, uint256[])",
|
||||
"function getSupportedChainCount() returns (uint256)",
|
||||
"function getSupportedChainId(uint256 _index) returns (uint256)",
|
||||
"function getVotes(address account) returns (uint256)",
|
||||
"function initializeLogoURI(string _logoURI)",
|
||||
"function initializer() returns (address)",
|
||||
"function isActive() returns (bool)",
|
||||
"function isChainSupported(uint256 _chainId) returns (bool)",
|
||||
"function isModuleActive(bytes32 _moduleId) returns (bool)",
|
||||
"function logo() returns (string)",
|
||||
"function logoURI() returns (string)",
|
||||
"function maxVotingDuration() returns (uint256)",
|
||||
"function minVotingDuration() returns (uint256)",
|
||||
"function modules(bytes32 ) returns (address)",
|
||||
"function name() returns (string)",
|
||||
"function nonces(address owner) returns (uint256)",
|
||||
"function numCheckpoints(address account) returns (uint32)",
|
||||
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
||||
"function proposalCounter() returns (uint256)",
|
||||
"function proposals(uint256 ) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, bytes, uint256, uint256)",
|
||||
"function quorumPercentage() returns (uint256)",
|
||||
"function supportedChainIds(uint256 ) returns (uint256)",
|
||||
"function supportedChains(uint256 ) returns (bool)",
|
||||
"function symbol() returns (string)",
|
||||
"function tokenURI() returns (string)",
|
||||
"function totalSupply() returns (uint256)",
|
||||
"function transfer(address , uint256 ) returns (bool)",
|
||||
"function transferFrom(address , address , uint256 ) returns (bool)",
|
||||
"function vote(uint256 _proposalId, bool _support)",
|
||||
"event Approval(address owner, address spender, uint256 value)",
|
||||
"event ChainAdded(uint256 chainId)",
|
||||
"event ChainRemoved(uint256 chainId)",
|
||||
"event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp)",
|
||||
"event DLEInitialized(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, address tokenAddress, uint256[] supportedChainIds)",
|
||||
"event DelegateChanged(address delegator, address fromDelegate, address toDelegate)",
|
||||
"event DelegateVotesChanged(address delegate, uint256 previousVotes, uint256 newVotes)",
|
||||
"event EIP712DomainChanged()",
|
||||
"event InitialTokensDistributed(address[] partners, uint256[] amounts)",
|
||||
"event LogoURIUpdated(string oldURI, string newURI)",
|
||||
"event ModuleAdded(bytes32 moduleId, address moduleAddress)",
|
||||
"event ModuleRemoved(bytes32 moduleId)",
|
||||
"event ProposalCancelled(uint256 proposalId, string reason)",
|
||||
"event ProposalCreated(uint256 proposalId, address initiator, string description)",
|
||||
"event ProposalExecuted(uint256 proposalId, bytes operation)",
|
||||
"event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId)",
|
||||
"event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId)",
|
||||
"event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains)",
|
||||
"event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower)",
|
||||
"event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage)",
|
||||
"event TokensTransferredByGovernance(address recipient, uint256 amount)",
|
||||
"event Transfer(address from, address to, uint256 value)",
|
||||
"event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration)",
|
||||
];
|
||||
|
||||
|
||||
// ABI для деактивации (специальные функции) - НЕ СУЩЕСТВУЮТ В КОНТРАКТЕ
|
||||
export const DLE_DEACTIVATION_ABI = [
|
||||
// Эти функции не существуют в контракте DLE
|
||||
];
|
||||
|
||||
// ABI для токенов (базовые функции)
|
||||
export const TOKEN_ABI = [
|
||||
"function balanceOf(address owner) view returns (uint256)",
|
||||
"function decimals() view returns (uint8)",
|
||||
"function totalSupply() view returns (uint256)"
|
||||
];
|
||||
@@ -12,6 +12,91 @@
|
||||
|
||||
import api from '@/api/axios';
|
||||
import { ethers } from 'ethers';
|
||||
import { DLE_ABI, DLE_DEACTIVATION_ABI, TOKEN_ABI } from './dle-abi';
|
||||
|
||||
// Функция для переключения сети кошелька
|
||||
export async function switchToVotingNetwork(chainId) {
|
||||
try {
|
||||
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId}...`);
|
||||
|
||||
// Конфигурации сетей
|
||||
const networks = {
|
||||
'11155111': { // Sepolia
|
||||
chainId: '0xaa36a7',
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://1rpc.io/sepolia'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
},
|
||||
'17000': { // Holesky
|
||||
chainId: '0x4268',
|
||||
chainName: 'Holesky',
|
||||
nativeCurrency: { name: 'Holesky Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://ethereum-holesky.publicnode.com'],
|
||||
blockExplorerUrls: ['https://holesky.etherscan.io']
|
||||
},
|
||||
'421614': { // Arbitrum Sepolia
|
||||
chainId: '0x66eee',
|
||||
chainName: 'Arbitrum Sepolia',
|
||||
nativeCurrency: { name: 'Arbitrum Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
|
||||
blockExplorerUrls: ['https://sepolia.arbiscan.io']
|
||||
},
|
||||
'84532': { // Base Sepolia
|
||||
chainId: '0x14a34',
|
||||
chainName: 'Base Sepolia',
|
||||
nativeCurrency: { name: 'Base Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://sepolia.base.org'],
|
||||
blockExplorerUrls: ['https://sepolia.basescan.org']
|
||||
}
|
||||
};
|
||||
|
||||
const networkConfig = networks[chainId];
|
||||
if (!networkConfig) {
|
||||
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем, подключена ли уже нужная сеть
|
||||
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
if (currentChainId === networkConfig.chainId) {
|
||||
console.log(`✅ [NETWORK] Сеть ${chainId} уже подключена`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Пытаемся переключиться на нужную сеть
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: networkConfig.chainId }]
|
||||
});
|
||||
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainId}`);
|
||||
return true;
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log(`➕ [NETWORK] Добавляем сеть ${chainId}...`);
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [networkConfig]
|
||||
});
|
||||
console.log(`✅ [NETWORK] Сеть ${chainId} добавлена и подключена`);
|
||||
return true;
|
||||
} catch (addError) {
|
||||
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainId}:`, addError);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainId}:`, switchError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [NETWORK] Общая ошибка переключения сети:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить подключение к браузерному кошельку
|
||||
@@ -60,6 +145,8 @@ export async function checkWalletConnection() {
|
||||
* Используется только система голосования (proposals)
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Получить информацию о DLE из блокчейна
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
@@ -109,12 +196,9 @@ export async function createProposal(dleAddress, proposalData) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для создания предложения
|
||||
const dleAbi = [
|
||||
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
|
||||
];
|
||||
// Используем общий ABI
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Создаем предложение
|
||||
const tx = await dle.createProposal(
|
||||
@@ -162,14 +246,111 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для голосования
|
||||
const dleAbi = [
|
||||
"function vote(uint256 _proposalId, bool _support) external"
|
||||
];
|
||||
// Используем общий ABI
|
||||
let dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
// Дополнительная диагностика перед голосованием
|
||||
try {
|
||||
console.log('🔍 [VOTE DEBUG] Проверяем состояние предложения...');
|
||||
const proposalState = await dle.getProposalState(proposalId);
|
||||
console.log('🔍 [VOTE DEBUG] Состояние предложения:', proposalState);
|
||||
|
||||
// Проверяем, можно ли голосовать (состояние должно быть 0 = Pending)
|
||||
if (Number(proposalState) !== 0) {
|
||||
throw new Error(`Предложение в состоянии ${proposalState}, голосование невозможно`);
|
||||
}
|
||||
|
||||
console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования');
|
||||
|
||||
// Проверяем сеть голосования
|
||||
try {
|
||||
const proposal = await dle.proposals(proposalId);
|
||||
const currentChainId = await dle.getCurrentChainId();
|
||||
const governanceChainId = proposal.governanceChainId;
|
||||
|
||||
console.log('🔍 [VOTE DEBUG] Текущая сеть контракта:', currentChainId.toString());
|
||||
console.log('🔍 [VOTE DEBUG] Сеть голосования предложения:', governanceChainId.toString());
|
||||
|
||||
if (currentChainId.toString() !== governanceChainId.toString()) {
|
||||
console.log('🔄 [VOTE DEBUG] Неправильная сеть! Пытаемся переключиться...');
|
||||
|
||||
// Пытаемся переключить сеть
|
||||
const switched = await switchToVotingNetwork(governanceChainId.toString());
|
||||
if (switched) {
|
||||
console.log('✅ [VOTE DEBUG] Сеть успешно переключена, переподключаемся к контракту...');
|
||||
|
||||
// Определяем правильный адрес контракта для сети голосования
|
||||
let correctContractAddress = dleAddress;
|
||||
|
||||
// Если контракт развернут в другой сети, нужно найти контракт в нужной сети
|
||||
if (currentChainId.toString() !== governanceChainId.toString()) {
|
||||
console.log('🔍 [VOTE DEBUG] Ищем контракт в сети голосования...');
|
||||
|
||||
try {
|
||||
// Получаем информацию о мультичейн развертывании из БД
|
||||
const response = await fetch('/api/dle-core/get-multichain-contracts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
originalContract: dleAddress,
|
||||
targetChainId: governanceChainId.toString()
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.contractAddress) {
|
||||
correctContractAddress = data.contractAddress;
|
||||
console.log('🔍 [VOTE DEBUG] Найден контракт в сети голосования:', correctContractAddress);
|
||||
} else {
|
||||
console.warn('⚠️ [VOTE DEBUG] Контракт в сети голосования не найден, используем исходный');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ [VOTE DEBUG] Ошибка получения контракта из БД, используем исходный');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ [VOTE DEBUG] Ошибка поиска контракта, используем исходный:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Переподключаемся к контракту в новой сети
|
||||
const newProvider = new ethers.BrowserProvider(window.ethereum);
|
||||
const newSigner = await newProvider.getSigner();
|
||||
dle = new ethers.Contract(correctContractAddress, DLE_ABI, newSigner);
|
||||
|
||||
// Проверяем, что теперь все корректно
|
||||
const newCurrentChainId = await dle.getCurrentChainId();
|
||||
console.log('🔍 [VOTE DEBUG] Новая текущая сеть контракта:', newCurrentChainId.toString());
|
||||
|
||||
if (newCurrentChainId.toString() === governanceChainId.toString()) {
|
||||
console.log('✅ [VOTE DEBUG] Сеть для голосования теперь корректна');
|
||||
} else {
|
||||
throw new Error(`Не удалось переключиться на правильную сеть. Текущая: ${newCurrentChainId}, требуется: ${governanceChainId}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Неправильная сеть! Контракт в сети ${currentChainId}, а голосование должно быть в сети ${governanceChainId}. Переключите кошелек вручную.`);
|
||||
}
|
||||
} else {
|
||||
console.log('🔍 [VOTE DEBUG] Сеть для голосования корректна');
|
||||
}
|
||||
|
||||
// Проверяем право голоса
|
||||
const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint);
|
||||
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
|
||||
if (votingPower === 0n) {
|
||||
throw new Error('У пользователя нет права голоса (votingPower = 0)');
|
||||
}
|
||||
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
|
||||
} catch (votingPowerError) {
|
||||
console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message);
|
||||
}
|
||||
|
||||
} catch (debugError) {
|
||||
console.warn('⚠️ [VOTE DEBUG] Ошибка диагностики (продолжаем):', debugError.message);
|
||||
}
|
||||
|
||||
// Голосуем за предложение
|
||||
console.log('🗳️ [VOTE] Отправляем транзакцию голосования...');
|
||||
const tx = await dle.vote(proposalId, support);
|
||||
|
||||
// Ждем подтверждения транзакции
|
||||
@@ -182,10 +363,40 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
||||
blockNumber: receipt.blockNumber
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка голосования:', error);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка голосования:', error);
|
||||
|
||||
// Детальная диагностика ошибки
|
||||
if (error.code === 'CALL_EXCEPTION' && error.data) {
|
||||
console.error('🔍 [ERROR DEBUG] Детали ошибки:', {
|
||||
code: error.code,
|
||||
data: error.data,
|
||||
reason: error.reason,
|
||||
action: error.action
|
||||
});
|
||||
|
||||
// Расшифровка кода ошибки
|
||||
if (error.data === '0x2eaf0f6d') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrWrongChain - неправильная сеть для голосования');
|
||||
} else if (error.data === '0xe7005635') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrAlreadyVoted - пользователь уже голосовал по этому предложению');
|
||||
} else if (error.data === '0x21c19873') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrNoPower - у пользователя нет права голоса');
|
||||
} else if (error.data === '0x834d7b85') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalMissing - предложение не найдено');
|
||||
} else if (error.data === '0xd6792fad') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalEnded - время голосования истекло');
|
||||
} else if (error.data === '0x2d686f73') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalExecuted - предложение уже исполнено');
|
||||
} else if (error.data === '0xc7567e07') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalCanceled - предложение отменено');
|
||||
} else {
|
||||
console.error('❌ [ERROR DEBUG] Неизвестная ошибка:', error.data);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,12 +417,9 @@ export async function executeProposal(dleAddress, proposalId) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для исполнения предложения
|
||||
const dleAbi = [
|
||||
"function executeProposal(uint256 _proposalId) external"
|
||||
];
|
||||
// Используем общий ABI
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Исполняем предложение
|
||||
const tx = await dle.executeProposal(proposalId);
|
||||
@@ -233,30 +441,112 @@ export async function executeProposal(dleAddress, proposalId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать предложение о добавлении модуля
|
||||
* Отменить предложение
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {string} reason - Причина отмены
|
||||
* @returns {Promise<Object>} - Результат отмены
|
||||
*/
|
||||
export async function cancelProposal(dleAddress, proposalId, reason) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
if (!window.ethereum) {
|
||||
throw new Error('Браузерный кошелек не установлен');
|
||||
}
|
||||
|
||||
// Запрашиваем подключение к кошельку
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// Используем общий ABI
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Отменяем предложение
|
||||
const tx = await dle.cancelProposal(proposalId, reason);
|
||||
|
||||
// Ждем подтверждения транзакции
|
||||
const receipt = await tx.wait();
|
||||
|
||||
console.log('Предложение отменено, tx hash:', tx.hash);
|
||||
|
||||
return {
|
||||
txHash: tx.hash,
|
||||
blockNumber: receipt.blockNumber
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Ошибка отмены предложения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить баланс токенов пользователя
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Баланс токенов
|
||||
*/
|
||||
export async function checkTokenBalance(dleAddress, userAddress) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
if (!window.ethereum) {
|
||||
throw new Error('Браузерный кошелек не установлен');
|
||||
}
|
||||
|
||||
// Создаем провайдер (только для чтения)
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, provider);
|
||||
|
||||
// Получаем баланс токенов
|
||||
const balance = await dle.balanceOf(userAddress);
|
||||
const balanceFormatted = ethers.formatEther(balance);
|
||||
|
||||
console.log(`💰 Баланс токенов для ${userAddress}: ${balanceFormatted}`);
|
||||
|
||||
return {
|
||||
balance: balanceFormatted,
|
||||
hasTokens: balance > 0,
|
||||
rawBalance: balance.toString()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки баланса токенов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать предложение о добавлении модуля (с автоматической оплатой газа)
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {string} description - Описание предложения
|
||||
* @param {number} duration - Длительность голосования в секундах
|
||||
* @param {string} moduleId - ID модуля
|
||||
* @param {string} moduleAddress - Адрес модуля
|
||||
* @param {number} chainId - ID цепочки для голосования
|
||||
* @param {string} deploymentId - ID деплоя для получения приватного ключа (опционально)
|
||||
* @returns {Promise<Object>} - Результат создания предложения
|
||||
*/
|
||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId, deploymentId = null) {
|
||||
try {
|
||||
const response = await api.post('/blockchain/create-add-module-proposal', {
|
||||
const requestData = {
|
||||
dleAddress: dleAddress,
|
||||
description: description,
|
||||
duration: duration,
|
||||
moduleId: moduleId,
|
||||
moduleAddress: moduleAddress,
|
||||
chainId: chainId
|
||||
});
|
||||
};
|
||||
|
||||
// Добавляем deploymentId если он передан
|
||||
if (deploymentId) {
|
||||
requestData.deploymentId = deploymentId;
|
||||
}
|
||||
|
||||
const response = await api.post('/dle-modules/create-add-module-proposal', requestData);
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.message || 'Не удалось создать предложение о добавлении модуля');
|
||||
throw new Error(response.data.error || 'Не удалось создать предложение о добавлении модуля');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания предложения о добавлении модуля:', error);
|
||||
@@ -537,6 +827,7 @@ export async function getSupportedChains(dleAddress) {
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат деактивации
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function deactivateDLE(dleAddress, userAddress) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
@@ -568,15 +859,9 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
||||
|
||||
console.log('Проверка деактивации прошла успешно, выполняем деактивацию...');
|
||||
|
||||
// ABI для деактивации DLE
|
||||
const dleAbi = [
|
||||
"function deactivate() external",
|
||||
"function balanceOf(address) external view returns (uint256)",
|
||||
"function totalSupply() external view returns (uint256)",
|
||||
"function isActive() external view returns (bool)"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Дополнительные проверки перед деактивацией
|
||||
const balance = await dle.balanceOf(userAddress);
|
||||
@@ -640,6 +925,7 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
||||
* @param {number} chainId - ID цепочки для деактивации
|
||||
* @returns {Promise<Object>} - Результат создания предложения
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function createDeactivationProposal(dleAddress, description, duration, chainId) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
@@ -650,11 +936,9 @@ export async function createDeactivationProposal(dleAddress, description, durati
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const dleAbi = [
|
||||
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||
|
||||
const tx = await dle.createDeactivationProposal(description, duration, chainId);
|
||||
const receipt = await tx.wait();
|
||||
@@ -681,6 +965,7 @@ export async function createDeactivationProposal(dleAddress, description, durati
|
||||
* @param {boolean} support - Поддержка предложения
|
||||
* @returns {Promise<Object>} - Результат голосования
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function voteDeactivationProposal(dleAddress, proposalId, support) {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
@@ -690,11 +975,9 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support)
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const dleAbi = [
|
||||
"function voteDeactivation(uint256 _proposalId, bool _support) external"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||
|
||||
const tx = await dle.voteDeactivation(proposalId, support);
|
||||
const receipt = await tx.wait();
|
||||
@@ -744,6 +1027,7 @@ export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
@@ -753,11 +1037,9 @@ export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const dleAbi = [
|
||||
"function executeDeactivationProposal(uint256 _proposalId) external"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||
|
||||
const tx = await dle.executeDeactivationProposal(proposalId);
|
||||
const receipt = await tx.wait();
|
||||
@@ -823,12 +1105,9 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для создания предложения
|
||||
const dleAbi = [
|
||||
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
|
||||
];
|
||||
// Используем общий ABI
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Кодируем операцию перевода токенов
|
||||
const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)");
|
||||
@@ -872,4 +1151,75 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
||||
console.error('Ошибка создания предложения о переводе токенов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исполнить мультиконтрактное предложение во всех целевых сетях
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export async function executeMultichainProposal(dleAddress, proposalId, userAddress) {
|
||||
try {
|
||||
// Импортируем сервис мультиконтрактного исполнения
|
||||
const {
|
||||
executeInAllTargetChains,
|
||||
getDeploymentId,
|
||||
formatExecutionResult,
|
||||
getExecutionErrors
|
||||
} = await import('@/services/multichainExecutionService');
|
||||
|
||||
// Получаем ID деплоя
|
||||
const deploymentId = await getDeploymentId(dleAddress);
|
||||
|
||||
// Исполняем во всех целевых сетях
|
||||
const result = await executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
summary: formatExecutionResult(result),
|
||||
errors: getExecutionErrors(result)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка исполнения мультиконтрактного предложения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исполнить мультиконтрактное предложение в конкретной сети
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {number} targetChainId - ID целевой сети
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export async function executeMultichainProposalInChain(dleAddress, proposalId, targetChainId, userAddress) {
|
||||
try {
|
||||
// Импортируем сервис мультиконтрактного исполнения
|
||||
const {
|
||||
executeInTargetChain,
|
||||
getDeploymentId,
|
||||
getChainName
|
||||
} = await import('@/services/multichainExecutionService');
|
||||
|
||||
// Получаем ID деплоя
|
||||
const deploymentId = await getDeploymentId(dleAddress);
|
||||
|
||||
// Исполняем в конкретной сети
|
||||
const result = await executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
chainName: getChainName(targetChainId)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка исполнения мультиконтрактного предложения в сети:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
105
frontend/src/utils/networkConfig.js
Normal file
105
frontend/src/utils/networkConfig.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Конфигурации сетей блокчейна для DLE
|
||||
*
|
||||
* Author: HB3 Accelerator
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
export const SUPPORTED_NETWORKS = {
|
||||
1: {
|
||||
chainId: '0x1',
|
||||
chainName: 'Ethereum Mainnet',
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://mainnet.infura.io/v3/'],
|
||||
blockExplorerUrls: ['https://etherscan.io'],
|
||||
},
|
||||
11155111: {
|
||||
chainId: '0xaa36a7',
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Sepolia Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://sepolia.infura.io/v3/', 'https://1rpc.io/sepolia'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io'],
|
||||
},
|
||||
17000: {
|
||||
chainId: '0x4268',
|
||||
chainName: 'Holesky',
|
||||
nativeCurrency: {
|
||||
name: 'Holesky Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://ethereum-holesky.publicnode.com'],
|
||||
blockExplorerUrls: ['https://holesky.etherscan.io'],
|
||||
},
|
||||
421614: {
|
||||
chainId: '0x66eee',
|
||||
chainName: 'Arbitrum Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Arbitrum Sepolia Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
|
||||
blockExplorerUrls: ['https://sepolia.arbiscan.io'],
|
||||
},
|
||||
84532: {
|
||||
chainId: '0x14a34',
|
||||
chainName: 'Base Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Base Sepolia Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://sepolia.base.org'],
|
||||
blockExplorerUrls: ['https://sepolia.basescan.org'],
|
||||
},
|
||||
8453: {
|
||||
chainId: '0x2105',
|
||||
chainName: 'Base',
|
||||
nativeCurrency: {
|
||||
name: 'Base Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://mainnet.base.org'],
|
||||
blockExplorerUrls: ['https://basescan.org'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить конфигурацию сети по chainId
|
||||
* @param {number|string} chainId - ID сети
|
||||
* @returns {Object|null} - Конфигурация сети или null
|
||||
*/
|
||||
export function getNetworkConfig(chainId) {
|
||||
const numericChainId = typeof chainId === 'string' ? parseInt(chainId, 16) : chainId;
|
||||
return SUPPORTED_NETWORKS[numericChainId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить hex представление chainId
|
||||
* @param {number} chainId - ID сети
|
||||
* @returns {string} - Hex представление
|
||||
*/
|
||||
export function getHexChainId(chainId) {
|
||||
return `0x${chainId.toString(16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, поддерживается ли сеть
|
||||
* @param {number|string} chainId - ID сети
|
||||
* @returns {boolean} - Поддерживается ли сеть
|
||||
*/
|
||||
export function isNetworkSupported(chainId) {
|
||||
return getNetworkConfig(chainId) !== null;
|
||||
}
|
||||
157
frontend/src/utils/networkSwitcher.js
Normal file
157
frontend/src/utils/networkSwitcher.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Утилиты для переключения сетей блокчейна
|
||||
*
|
||||
* Author: HB3 Accelerator
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import { getNetworkConfig, getHexChainId, isNetworkSupported } from './networkConfig.js';
|
||||
|
||||
/**
|
||||
* Переключить сеть в MetaMask
|
||||
* @param {number} targetChainId - ID целевой сети
|
||||
* @returns {Promise<Object>} - Результат переключения
|
||||
*/
|
||||
export async function switchNetwork(targetChainId) {
|
||||
try {
|
||||
console.log(`🔄 [Network Switch] Переключаемся на сеть ${targetChainId}...`);
|
||||
|
||||
// Проверяем, поддерживается ли сеть
|
||||
if (!isNetworkSupported(targetChainId)) {
|
||||
throw new Error(`Сеть ${targetChainId} не поддерживается`);
|
||||
}
|
||||
|
||||
// Проверяем наличие MetaMask
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||
}
|
||||
|
||||
// Получаем конфигурацию сети
|
||||
const networkConfig = getNetworkConfig(targetChainId);
|
||||
if (!networkConfig) {
|
||||
throw new Error(`Конфигурация для сети ${targetChainId} не найдена`);
|
||||
}
|
||||
|
||||
// Проверяем текущую сеть
|
||||
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
console.log(`🔄 [Network Switch] Текущая сеть: ${currentChainId}, Целевая: ${getHexChainId(targetChainId)}`);
|
||||
|
||||
// Если уже в нужной сети, возвращаем успех
|
||||
if (currentChainId === getHexChainId(targetChainId)) {
|
||||
console.log(`✅ [Network Switch] Уже в сети ${targetChainId}`);
|
||||
return {
|
||||
success: true,
|
||||
message: `Уже в сети ${networkConfig.chainName}`,
|
||||
chainId: targetChainId,
|
||||
chainName: networkConfig.chainName
|
||||
};
|
||||
}
|
||||
|
||||
// Пытаемся переключиться на существующую сеть
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: getHexChainId(targetChainId) }],
|
||||
});
|
||||
|
||||
console.log(`✅ [Network Switch] Успешно переключились на ${networkConfig.chainName}`);
|
||||
return {
|
||||
success: true,
|
||||
message: `Переключились на ${networkConfig.chainName}`,
|
||||
chainId: targetChainId,
|
||||
chainName: networkConfig.chainName
|
||||
};
|
||||
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена в MetaMask, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log(`➕ [Network Switch] Добавляем сеть ${networkConfig.chainName} в MetaMask...`);
|
||||
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: getHexChainId(targetChainId),
|
||||
chainName: networkConfig.chainName,
|
||||
nativeCurrency: networkConfig.nativeCurrency,
|
||||
rpcUrls: networkConfig.rpcUrls,
|
||||
blockExplorerUrls: networkConfig.blockExplorerUrls,
|
||||
}],
|
||||
});
|
||||
|
||||
console.log(`✅ [Network Switch] Сеть ${networkConfig.chainName} добавлена и активирована`);
|
||||
return {
|
||||
success: true,
|
||||
message: `Сеть ${networkConfig.chainName} добавлена и активирована`,
|
||||
chainId: targetChainId,
|
||||
chainName: networkConfig.chainName
|
||||
};
|
||||
|
||||
} catch (addError) {
|
||||
console.error(`❌ [Network Switch] Ошибка добавления сети:`, addError);
|
||||
throw new Error(`Не удалось добавить сеть ${networkConfig.chainName}: ${addError.message}`);
|
||||
}
|
||||
} else {
|
||||
// Другие ошибки переключения
|
||||
console.error(`❌ [Network Switch] Ошибка переключения сети:`, switchError);
|
||||
throw new Error(`Не удалось переключиться на ${networkConfig.chainName}: ${switchError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [Network Switch] Ошибка:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
chainId: targetChainId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить текущую сеть
|
||||
* @returns {Promise<Object>} - Информация о текущей сети
|
||||
*/
|
||||
export async function getCurrentNetwork() {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден');
|
||||
}
|
||||
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
const numericChainId = parseInt(chainId, 16);
|
||||
const networkConfig = getNetworkConfig(numericChainId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
chainId: numericChainId,
|
||||
hexChainId: chainId,
|
||||
chainName: networkConfig?.chainName || 'Неизвестная сеть',
|
||||
isSupported: isNetworkSupported(numericChainId)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [Network Check] Ошибка:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список поддерживаемых сетей
|
||||
* @returns {Array} - Список поддерживаемых сетей
|
||||
*/
|
||||
export function getSupportedNetworks() {
|
||||
return Object.entries(SUPPORTED_NETWORKS).map(([chainId, config]) => ({
|
||||
chainId: parseInt(chainId),
|
||||
hexChainId: getHexChainId(parseInt(chainId)),
|
||||
chainName: config.chainName,
|
||||
nativeCurrency: config.nativeCurrency,
|
||||
rpcUrls: config.rpcUrls,
|
||||
blockExplorerUrls: config.blockExplorerUrls
|
||||
}));
|
||||
}
|
||||
@@ -37,6 +37,9 @@ class WebSocketClient {
|
||||
console.log('[WebSocket] Подключение установлено');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Уведомляем о подключении
|
||||
this.emit('connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
@@ -120,6 +123,15 @@ class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Эмиссия события
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Алиас для on() - для совместимости с useDeploymentWebSocket
|
||||
subscribe(event, callback) {
|
||||
this.on(event, callback);
|
||||
|
||||
@@ -156,12 +156,8 @@ let unsubscribe = null;
|
||||
onMounted(() => {
|
||||
// console.log('[CrmView] Компонент загружен');
|
||||
|
||||
// Если пользователь авторизован, загружаем данные
|
||||
if (auth.isAuthenticated.value) {
|
||||
loadDLEs();
|
||||
} else {
|
||||
isLoading.value = false;
|
||||
}
|
||||
// Загружаем DLE для всех пользователей (авторизованных и неавторизованных)
|
||||
loadDLEs();
|
||||
|
||||
// Подписка на события авторизации
|
||||
unsubscribe = eventBus.on('auth-state-changed', handleAuthEvent);
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="isLoadingDles" class="loading-dles">
|
||||
<p>Загрузка деплоированных DLE...</p>
|
||||
</div>
|
||||
@@ -56,6 +57,7 @@
|
||||
class="dle-card"
|
||||
@click="openDleManagement(dle.dleAddress)"
|
||||
>
|
||||
|
||||
<div class="dle-header">
|
||||
<div class="dle-title-section">
|
||||
<img
|
||||
@@ -99,18 +101,18 @@
|
||||
<strong>Адреса контрактов:</strong>
|
||||
<div class="addresses-list">
|
||||
<div
|
||||
v-for="network in dle.deployedNetworks || [{ chainId: 11155111, address: dle.dleAddress }]"
|
||||
:key="network.chainId"
|
||||
v-for="chainId in (dle.supportedChainIds || [11155111])"
|
||||
:key="chainId"
|
||||
class="address-item"
|
||||
>
|
||||
<span class="chain-name">{{ getChainName(network.chainId) }}:</span>
|
||||
<span class="chain-name">{{ getChainName(chainId) }}:</span>
|
||||
<a
|
||||
:href="getExplorerUrl(network.chainId, network.address)"
|
||||
:href="getExplorerUrl(chainId, dle.dleAddress)"
|
||||
target="_blank"
|
||||
class="address-link"
|
||||
@click.stop
|
||||
>
|
||||
{{ shortenAddress(network.address) }}
|
||||
{{ shortenAddress(dle.dleAddress) }}
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -253,14 +255,22 @@ async function loadDeployedDles() {
|
||||
const dlesWithBlockchainData = await Promise.all(
|
||||
dlesFromApi.map(async (dle) => {
|
||||
try {
|
||||
console.log(`[ManagementView] Читаем данные из блокчейна для ${dle.dleAddress}`);
|
||||
// Используем адрес из deployedNetworks если dleAddress null
|
||||
const dleAddress = dle.dleAddress || (dle.deployedNetworks && dle.deployedNetworks.length > 0 ? dle.deployedNetworks[0].address : null);
|
||||
|
||||
if (!dleAddress) {
|
||||
console.warn(`[ManagementView] Нет адреса для DLE ${dle.deployment_id || 'unknown'}`);
|
||||
return dle;
|
||||
}
|
||||
|
||||
console.log(`[ManagementView] Читаем данные из блокчейна для ${dleAddress}`);
|
||||
|
||||
// Читаем данные из блокчейна
|
||||
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
|
||||
dleAddress: dle.dleAddress
|
||||
dleAddress: dleAddress
|
||||
});
|
||||
|
||||
console.log(`[ManagementView] Ответ от блокчейна для ${dle.dleAddress}:`, blockchainResponse.data);
|
||||
console.log(`[ManagementView] Ответ от блокчейна для ${dleAddress}:`, blockchainResponse.data);
|
||||
|
||||
if (blockchainResponse.data.success) {
|
||||
const blockchainData = blockchainResponse.data.data;
|
||||
@@ -376,7 +386,15 @@ function formatTokenAmount(amount) {
|
||||
const num = parseFloat(amount);
|
||||
if (num === 0) return '0';
|
||||
|
||||
// Всегда показываем полное число с разделителями тысяч
|
||||
// Для очень маленьких чисел показываем с большей точностью
|
||||
if (num < 1) {
|
||||
return num.toLocaleString('ru-RU', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 18
|
||||
});
|
||||
}
|
||||
|
||||
// Для больших чисел показываем с разделителями тысяч
|
||||
return num.toLocaleString('ru-RU', { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
@@ -812,6 +830,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.dle-title-section {
|
||||
|
||||
@@ -84,8 +84,11 @@
|
||||
v-model.number="newToken.minBalance"
|
||||
class="form-control"
|
||||
placeholder="0"
|
||||
min="0"
|
||||
step="0.01"
|
||||
:disabled="!canEdit"
|
||||
>
|
||||
<small class="form-text">Минимальный баланс токена для получения доступа</small>
|
||||
</div>
|
||||
|
||||
<!-- Настройки прав доступа -->
|
||||
@@ -158,6 +161,12 @@ async function addToken() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация порогов доступа
|
||||
if (newToken.readonlyThreshold >= newToken.editorThreshold) {
|
||||
alert('Минимум токенов для Read-Only доступа должен быть меньше минимума для Editor доступа');
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = {
|
||||
name: newToken.name,
|
||||
address: newToken.address,
|
||||
|
||||
@@ -66,34 +66,68 @@ const emailAuth = {
|
||||
<style scoped>
|
||||
.webssh-settings-block {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
margin: 2rem auto;
|
||||
max-width: 1000px;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #bbb;
|
||||
transition: color 0.2s;
|
||||
color: #666;
|
||||
padding: 0.5rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: #666;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.webssh-settings-block {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1413
frontend/src/views/smartcontracts/AddModuleFormView.vue
Normal file
1413
frontend/src/views/smartcontracts/AddModuleFormView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,91 +30,29 @@
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Блоки операций DLE -->
|
||||
<div class="operations-blocks">
|
||||
<div class="blocks-header">
|
||||
<h4>Типы операций DLE контракта</h4>
|
||||
<p>Выберите тип операции для создания предложения</p>
|
||||
<!-- Информация для неавторизованных пользователей -->
|
||||
<div v-if="!props.isAuthenticated" class="auth-notice">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
|
||||
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
|
||||
</div>
|
||||
|
||||
<!-- Информация для неавторизованных пользователей -->
|
||||
<div v-if="!props.isAuthenticated" class="auth-notice">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Для создания предложений необходимо авторизоваться в приложении</strong>
|
||||
<p class="mb-0 mt-2">Подключите кошелек в сайдбаре для создания новых предложений</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Блоки операций -->
|
||||
<div class="operations-grid">
|
||||
<!-- Управление токенами -->
|
||||
</div>
|
||||
|
||||
<!-- Блоки операций -->
|
||||
<div class="operations-grid">
|
||||
<!-- Основные операции DLE -->
|
||||
<div class="operation-category">
|
||||
<h5>💸 Управление токенами</h5>
|
||||
<h5>Основные операции DLE</h5>
|
||||
<div class="operation-blocks">
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">💸</div>
|
||||
<h6>Передача токенов</h6>
|
||||
<p>Перевод токенов DLE другому адресу через governance</p>
|
||||
<button class="create-btn" @click="openTransferForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Управление модулями -->
|
||||
<div class="operation-category">
|
||||
<h5>🔧 Управление модулями</h5>
|
||||
<div class="operation-blocks">
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">➕</div>
|
||||
<h6>Добавить модуль</h6>
|
||||
<p>Добавление нового модуля в DLE контракт</p>
|
||||
<button class="create-btn" @click="openAddModuleForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">➖</div>
|
||||
<h6>Удалить модуль</h6>
|
||||
<p>Удаление существующего модуля из DLE контракта</p>
|
||||
<button class="create-btn" @click="openRemoveModuleForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Управление сетями -->
|
||||
<div class="operation-category">
|
||||
<h5>🌐 Управление сетями</h5>
|
||||
<div class="operation-blocks">
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">➕</div>
|
||||
<h6>Добавить сеть</h6>
|
||||
<p>Добавление новой поддерживаемой блокчейн сети</p>
|
||||
<button class="create-btn" @click="openAddChainForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">➖</div>
|
||||
<h6>Удалить сеть</h6>
|
||||
<p>Удаление поддерживаемой блокчейн сети</p>
|
||||
<button class="create-btn" @click="openRemoveChainForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Управление настройками DLE -->
|
||||
<div class="operation-category">
|
||||
<h5>⚙️ Настройки DLE</h5>
|
||||
<div class="operation-blocks">
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">📝</div>
|
||||
<h6>Обновить данные DLE</h6>
|
||||
<p>Изменение основной информации о DLE (название, символ, адрес и т.д.)</p>
|
||||
<button class="create-btn" @click="openUpdateDLEInfoForm" :disabled="!props.isAuthenticated">
|
||||
@@ -122,7 +60,6 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">📊</div>
|
||||
<h6>Изменить кворум</h6>
|
||||
<p>Изменение процента голосов, необходимого для принятия решений</p>
|
||||
<button class="create-btn" @click="openUpdateQuorumForm" :disabled="!props.isAuthenticated">
|
||||
@@ -130,7 +67,6 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">⏰</div>
|
||||
<h6>Изменить время голосования</h6>
|
||||
<p>Настройка минимального и максимального времени голосования</p>
|
||||
<button class="create-btn" @click="openUpdateVotingDurationsForm" :disabled="!props.isAuthenticated">
|
||||
@@ -138,7 +74,41 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">🖼️</div>
|
||||
<h6>Оффчейн действие</h6>
|
||||
<p>Создание предложения для выполнения оффчейн операций в приложении</p>
|
||||
<button class="create-btn" @click="openOffchainActionForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<h6>Добавить модуль</h6>
|
||||
<p>Добавление нового модуля в DLE контракт</p>
|
||||
<button class="create-btn" @click="openAddModuleForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<h6>Удалить модуль</h6>
|
||||
<p>Удаление существующего модуля из DLE контракта</p>
|
||||
<button class="create-btn" @click="openRemoveModuleForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<h6>Добавить сеть</h6>
|
||||
<p>Добавление новой поддерживаемой блокчейн сети</p>
|
||||
<button class="create-btn" @click="openAddChainForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<h6>Удалить сеть</h6>
|
||||
<p>Удаление поддерживаемой блокчейн сети</p>
|
||||
<button class="create-btn" @click="openRemoveChainForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
<div class="operation-block">
|
||||
<h6>Изменить логотип</h6>
|
||||
<p>Обновление URI логотипа DLE для отображения в блокчейн-сканерах</p>
|
||||
<button class="create-btn" @click="openSetLogoURIForm" :disabled="!props.isAuthenticated">
|
||||
@@ -166,10 +136,8 @@
|
||||
:key="operation.id"
|
||||
class="operation-block module-operation-block"
|
||||
>
|
||||
<div class="operation-icon">{{ operation.icon }}</div>
|
||||
<h6>{{ operation.name }}</h6>
|
||||
<p>{{ operation.description }}</p>
|
||||
<div class="operation-category-tag">{{ operation.category }}</div>
|
||||
<button
|
||||
class="create-btn"
|
||||
@click="openModuleOperationForm(moduleOperation.moduleType, operation)"
|
||||
@@ -182,21 +150,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Оффчейн операции -->
|
||||
<div class="operation-category">
|
||||
<h5>📋 Оффчейн операции</h5>
|
||||
<div class="operation-blocks">
|
||||
<div class="operation-block">
|
||||
<div class="operation-icon">📄</div>
|
||||
<h6>Оффчейн действие</h6>
|
||||
<p>Создание предложения для выполнения оффчейн операций в приложении</p>
|
||||
<button class="create-btn" @click="openOffchainActionForm" :disabled="!props.isAuthenticated">
|
||||
Создать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -259,7 +212,6 @@ const availableChains = ref([]);
|
||||
// Состояние модулей и их операций
|
||||
const moduleOperations = ref([]);
|
||||
const isLoadingModuleOperations = ref(false);
|
||||
const modulesWebSocket = ref(null);
|
||||
const isModulesWSConnected = ref(false);
|
||||
|
||||
// Функции для открытия отдельных форм операций
|
||||
@@ -269,8 +221,11 @@ function openTransferForm() {
|
||||
}
|
||||
|
||||
function openAddModuleForm() {
|
||||
// TODO: Открыть форму для добавления модуля
|
||||
alert('Форма добавления модуля будет реализована');
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/add-module?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/add-module');
|
||||
}
|
||||
}
|
||||
|
||||
function openRemoveModuleForm() {
|
||||
@@ -325,13 +280,7 @@ function openModuleOperationForm(moduleType, operation) {
|
||||
|
||||
// Получить иконку для типа модуля
|
||||
function getModuleIcon(moduleType) {
|
||||
const icons = {
|
||||
treasury: '💰',
|
||||
timelock: '⏰',
|
||||
reader: '📖',
|
||||
hierarchicalVoting: '🗳️'
|
||||
};
|
||||
return icons[moduleType] || '🔧';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Функции
|
||||
@@ -364,6 +313,9 @@ async function loadDleData() {
|
||||
// Загружаем операции модулей
|
||||
await loadModuleOperations();
|
||||
|
||||
// Повторно подписываемся на обновления модулей для нового DLE
|
||||
resubscribeToModules();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
|
||||
} finally {
|
||||
@@ -401,49 +353,61 @@ async function loadModuleOperations() {
|
||||
|
||||
// WebSocket функции для модулей
|
||||
function connectModulesWebSocket() {
|
||||
if (modulesWebSocket.value && modulesWebSocket.value.readyState === WebSocket.OPEN) {
|
||||
if (isModulesWSConnected.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wsUrl = `ws://localhost:8000/ws/deployment`;
|
||||
modulesWebSocket.value = new WebSocket(wsUrl);
|
||||
|
||||
modulesWebSocket.value.onopen = () => {
|
||||
console.log('[CreateProposalView] WebSocket модулей соединение установлено');
|
||||
isModulesWSConnected.value = true;
|
||||
try {
|
||||
// Подключаемся через существующий WebSocket клиент
|
||||
wsClient.connect();
|
||||
|
||||
// Подписываемся на обновления модулей для текущего DLE
|
||||
if (dleAddress.value) {
|
||||
modulesWebSocket.value.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
dleAddress: dleAddress.value
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
modulesWebSocket.value.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
// Подписываемся на события deployment_update
|
||||
wsClient.on('deployment_update', (data) => {
|
||||
console.log('[CreateProposalView] Получено обновление деплоя:', data);
|
||||
handleModulesWebSocketMessage(data);
|
||||
} catch (error) {
|
||||
console.error('[CreateProposalView] Ошибка парсинга WebSocket сообщения модулей:', error);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
modulesWebSocket.value.onclose = () => {
|
||||
console.log('[CreateProposalView] WebSocket модулей соединение закрыто');
|
||||
// Подписываемся на подтверждение подписки
|
||||
wsClient.on('subscribed', (data) => {
|
||||
console.log('[CreateProposalView] Подписка подтверждена:', data);
|
||||
});
|
||||
|
||||
// Подписываемся на обновления модулей
|
||||
wsClient.on('modules_updated', (data) => {
|
||||
console.log('[CreateProposalView] Модули обновлены:', data);
|
||||
// Перезагружаем операции модулей при обновлении
|
||||
loadModuleOperations();
|
||||
});
|
||||
|
||||
// Подписываемся на статус деплоя
|
||||
wsClient.on('deployment_status', (data) => {
|
||||
console.log('[CreateProposalView] Статус деплоя:', data);
|
||||
handleModulesWebSocketMessage(data);
|
||||
});
|
||||
|
||||
// Подписываемся на событие подключения
|
||||
wsClient.on('connected', () => {
|
||||
console.log('[CreateProposalView] WebSocket подключен, подписываемся на модули');
|
||||
if (dleAddress.value) {
|
||||
wsClient.ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
dleAddress: dleAddress.value
|
||||
}));
|
||||
console.log('[CreateProposalView] Подписка на модули отправлена для DLE:', dleAddress.value);
|
||||
}
|
||||
});
|
||||
|
||||
isModulesWSConnected.value = true;
|
||||
console.log('[CreateProposalView] WebSocket модулей соединение установлено');
|
||||
} catch (error) {
|
||||
console.error('[CreateProposalView] Ошибка подключения WebSocket модулей:', error);
|
||||
isModulesWSConnected.value = false;
|
||||
|
||||
// Переподключаемся через 5 секунд
|
||||
setTimeout(() => {
|
||||
connectModulesWebSocket();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
modulesWebSocket.value.onerror = (error) => {
|
||||
console.error('[CreateProposalView] Ошибка WebSocket модулей:', error);
|
||||
isModulesWSConnected.value = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function handleModulesWebSocketMessage(data) {
|
||||
@@ -471,10 +435,30 @@ function handleModulesWebSocketMessage(data) {
|
||||
}
|
||||
|
||||
function disconnectModulesWebSocket() {
|
||||
if (modulesWebSocket.value) {
|
||||
modulesWebSocket.value.close();
|
||||
modulesWebSocket.value = null;
|
||||
if (isModulesWSConnected.value) {
|
||||
// Отписываемся от всех событий
|
||||
wsClient.off('deployment_update');
|
||||
wsClient.off('subscribed');
|
||||
wsClient.off('modules_updated');
|
||||
wsClient.off('deployment_status');
|
||||
wsClient.off('connected');
|
||||
|
||||
isModulesWSConnected.value = false;
|
||||
console.log('[CreateProposalView] WebSocket модулей отключен');
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для повторной подписки при изменении DLE адреса
|
||||
function resubscribeToModules() {
|
||||
if (isModulesWSConnected.value && wsClient.ws && wsClient.ws.readyState === WebSocket.OPEN && dleAddress.value) {
|
||||
wsClient.ws.send(JSON.stringify({
|
||||
type: 'subscribe',
|
||||
dleAddress: dleAddress.value
|
||||
}));
|
||||
console.log('[CreateProposalView] Повторная подписка на модули для DLE:', dleAddress.value);
|
||||
} else if (wsClient.ws && wsClient.ws.readyState === WebSocket.CONNECTING) {
|
||||
// Если соединение еще устанавливается, ждем события подключения
|
||||
console.log('[CreateProposalView] WebSocket еще подключается, ждем события connected');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,32 +542,6 @@ onUnmounted(() => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Стили для блоков операций */
|
||||
.operations-blocks {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
border: 1px solid #e9ecef;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.blocks-header {
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blocks-header h4 {
|
||||
color: var(--color-primary);
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.blocks-header p {
|
||||
color: #6c757d;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.auth-notice {
|
||||
margin-bottom: 2rem;
|
||||
@@ -626,110 +584,76 @@ onUnmounted(() => {
|
||||
.operation-category h5 {
|
||||
color: var(--color-primary);
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.operation-blocks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.operation-block {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border: 2px solid #e9ecef;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operation-block::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--color-primary), #20c997);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.operation-block:hover {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 8px 25px rgba(0, 123, 255, 0.15);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.operation-block:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.operation-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.operation-block h6 {
|
||||
color: #333;
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.operation-block p {
|
||||
color: #666;
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background: linear-gradient(135deg, var(--color-primary), #20c997);
|
||||
color: white;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transition: all 0.2s;
|
||||
min-width: 120px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.create-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
background: linear-gradient(135deg, #0056b3, #1ea085);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.create-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.create-btn:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
@@ -737,10 +661,6 @@ onUnmounted(() => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.create-btn:disabled::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Стили для модулей */
|
||||
.module-description {
|
||||
color: #666;
|
||||
@@ -751,54 +671,13 @@ onUnmounted(() => {
|
||||
|
||||
.module-operation-block {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
||||
border: 2px solid #e9ecef;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.module-operation-block::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #28a745, #20c997);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.module-operation-block:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.operation-category-tag {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Анимация появления модулей */
|
||||
.operation-category {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Индикатор загрузки модулей */
|
||||
.loading-modules {
|
||||
@@ -828,10 +707,6 @@ onUnmounted(() => {
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.operations-blocks {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.operation-blocks {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -840,14 +715,6 @@ onUnmounted(() => {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.operation-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.blocks-header h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.operation-category h5 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@@ -32,42 +32,30 @@
|
||||
|
||||
<!-- Блоки управления -->
|
||||
<div class="management-blocks">
|
||||
<!-- Первый ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block create-proposal-block">
|
||||
<!-- Столбец 1 -->
|
||||
<div class="blocks-column">
|
||||
<div class="management-block">
|
||||
<h3>Создать предложение</h3>
|
||||
<p>Универсальная форма для создания новых предложений</p>
|
||||
<button class="details-btn create-btn" @click="openCreateProposal">
|
||||
<button class="details-btn" @click="openCreateProposal">
|
||||
Подробнее
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Предложения</h3>
|
||||
<p>Создание, подписание, выполнение</p>
|
||||
<button class="details-btn" @click="openProposals">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Токены DLE</h3>
|
||||
<p>Балансы, трансферы, распределение</p>
|
||||
<button class="details-btn" @click="openTokens">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Второй ряд -->
|
||||
<div class="blocks-row">
|
||||
<div class="management-block">
|
||||
<h3>Кворум</h3>
|
||||
<p>Настройки голосования</p>
|
||||
<button class="details-btn" @click="openQuorum">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Модули DLE</h3>
|
||||
<p>Установка, настройка, управление</p>
|
||||
<button class="details-btn" @click="openModules">Подробнее</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Столбец 2 -->
|
||||
<div class="blocks-column">
|
||||
<div class="management-block">
|
||||
<h3>Предложения</h3>
|
||||
<p>Создание, подписание, выполнение</p>
|
||||
<button class="details-btn" @click="openProposals">Подробнее</button>
|
||||
</div>
|
||||
|
||||
<div class="management-block">
|
||||
<h3>Аналитика</h3>
|
||||
@@ -76,8 +64,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Третий ряд -->
|
||||
<div class="blocks-row">
|
||||
<!-- Столбец 3 -->
|
||||
<div class="blocks-column">
|
||||
<div class="management-block">
|
||||
<h3>История</h3>
|
||||
<p>Лог операций, события, транзакции</p>
|
||||
@@ -125,21 +113,6 @@ const openProposals = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openTokens = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/tokens?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/tokens');
|
||||
}
|
||||
};
|
||||
|
||||
const openQuorum = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/quorum?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management/quorum');
|
||||
}
|
||||
};
|
||||
|
||||
const openModules = () => {
|
||||
if (dleAddress.value) {
|
||||
@@ -236,15 +209,16 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.management-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.blocks-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
.blocks-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.management-block {
|
||||
@@ -255,6 +229,10 @@ onMounted(() => {
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.management-block:hover {
|
||||
@@ -268,6 +246,7 @@ onMounted(() => {
|
||||
color: var(--color-primary);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.management-block p {
|
||||
@@ -275,6 +254,7 @@ onMounted(() => {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.details-btn {
|
||||
@@ -288,6 +268,8 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
min-width: 120px;
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.details-btn:hover {
|
||||
@@ -295,35 +277,16 @@ onMounted(() => {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Стили для блока создания предложения */
|
||||
.create-proposal-block {
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%);
|
||||
border: 2px solid #28a745;
|
||||
}
|
||||
|
||||
.create-proposal-block:hover {
|
||||
border-color: #20c997;
|
||||
box-shadow: 0 4px 20px rgba(40, 167, 69, 0.15);
|
||||
}
|
||||
|
||||
.create-proposal-block h3 {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
background: linear-gradient(135deg, #218838, #1ea085);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 1024px) {
|
||||
.management-blocks {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.blocks-row {
|
||||
.management-blocks {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,598 +0,0 @@
|
||||
<!--
|
||||
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="quorum-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Кворум</h1>
|
||||
<p>Настройки голосования и кворума</p>
|
||||
</div>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Текущие настройки -->
|
||||
<div class="current-settings-section">
|
||||
<h2>Текущие настройки</h2>
|
||||
<div class="settings-grid">
|
||||
<div class="setting-card">
|
||||
<h3>Процент кворума</h3>
|
||||
<p class="setting-value">{{ currentQuorum }}%</p>
|
||||
<p class="setting-description">Минимальный процент токенов для принятия решения</p>
|
||||
</div>
|
||||
<div class="setting-card">
|
||||
<h3>Задержка голосования</h3>
|
||||
<p class="setting-value">{{ votingDelay }} блоков</p>
|
||||
<p class="setting-description">Время между созданием и началом голосования</p>
|
||||
</div>
|
||||
<div class="setting-card">
|
||||
<h3>Период голосования</h3>
|
||||
<p class="setting-value">{{ votingPeriod }} блоков</p>
|
||||
<p class="setting-description">Длительность периода голосования</p>
|
||||
</div>
|
||||
<div class="setting-card">
|
||||
<h3>Порог предложений</h3>
|
||||
<p class="setting-value">{{ proposalThreshold }} токенов</p>
|
||||
<p class="setting-description">Минимальное количество токенов для создания предложения</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Изменение настроек -->
|
||||
<div class="change-settings-section">
|
||||
<h2>Изменить настройки</h2>
|
||||
<form @submit.prevent="updateSettings" class="settings-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="newQuorum">Новый процент кворума:</label>
|
||||
<input
|
||||
id="newQuorum"
|
||||
v-model="newSettings.quorumPercentage"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
placeholder="51"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">% (от 1 до 100)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newVotingDelay">Новая задержка голосования:</label>
|
||||
<input
|
||||
id="newVotingDelay"
|
||||
v-model="newSettings.votingDelay"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="1"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">блоков</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="newVotingPeriod">Новый период голосования:</label>
|
||||
<input
|
||||
id="newVotingPeriod"
|
||||
v-model="newSettings.votingPeriod"
|
||||
type="number"
|
||||
min="1"
|
||||
placeholder="45818"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">блоков (~1 неделя)</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="newProposalThreshold">Новый порог предложений:</label>
|
||||
<input
|
||||
id="newProposalThreshold"
|
||||
v-model="newSettings.proposalThreshold"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="100"
|
||||
required
|
||||
>
|
||||
<span class="input-hint">токенов</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="changeReason">Причина изменения:</label>
|
||||
<textarea
|
||||
id="changeReason"
|
||||
v-model="newSettings.reason"
|
||||
placeholder="Опишите причину изменения настроек..."
|
||||
rows="4"
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isUpdating">
|
||||
{{ isUpdating ? 'Обновление...' : 'Обновить настройки' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- История изменений -->
|
||||
<div class="history-section">
|
||||
<h2>История изменений</h2>
|
||||
<div v-if="settingsHistory.length === 0" class="empty-state">
|
||||
<p>Нет истории изменений настроек</p>
|
||||
</div>
|
||||
<div v-else class="history-list">
|
||||
<div
|
||||
v-for="change in settingsHistory"
|
||||
:key="change.id"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="history-header">
|
||||
<h3>Изменение #{{ change.id }}</h3>
|
||||
<span class="change-date">{{ formatDate(change.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="change-details">
|
||||
<p><strong>Причина:</strong> {{ change.reason }}</p>
|
||||
<div class="changes-list">
|
||||
<div v-if="change.quorumChange" class="change-item">
|
||||
<span class="change-label">Кворум:</span>
|
||||
<span class="change-value">{{ change.quorumChange.from }}% → {{ change.quorumChange.to }}%</span>
|
||||
</div>
|
||||
<div v-if="change.votingDelayChange" class="change-item">
|
||||
<span class="change-label">Задержка голосования:</span>
|
||||
<span class="change-value">{{ change.votingDelayChange.from }} → {{ change.votingDelayChange.to }} блоков</span>
|
||||
</div>
|
||||
<div v-if="change.votingPeriodChange" class="change-item">
|
||||
<span class="change-label">Период голосования:</span>
|
||||
<span class="change-value">{{ change.votingPeriodChange.from }} → {{ change.votingPeriodChange.to }} блоков</span>
|
||||
</div>
|
||||
<div v-if="change.proposalThresholdChange" class="change-item">
|
||||
<span class="change-label">Порог предложений:</span>
|
||||
<span class="change-value">{{ change.proposalThresholdChange.from }} → {{ change.proposalThresholdChange.to }} токенов</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="change-author">
|
||||
<span>Автор: {{ formatAddress(change.author) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import { getGovernanceParams } from '../../services/dleV2Service.js';
|
||||
import { getQuorumAt, getVotingPowerAt } from '../../services/proposalsService.js';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Получаем адрес DLE из URL
|
||||
const dleAddress = computed(() => {
|
||||
return route.query.address;
|
||||
});
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние
|
||||
const isUpdating = ref(false);
|
||||
|
||||
// Текущие настройки
|
||||
const currentQuorum = ref(51);
|
||||
const votingDelay = ref(1);
|
||||
const votingPeriod = ref(45818);
|
||||
const proposalThreshold = ref(100);
|
||||
|
||||
// Новые настройки
|
||||
const newSettings = ref({
|
||||
quorumPercentage: '',
|
||||
votingDelay: '',
|
||||
votingPeriod: '',
|
||||
proposalThreshold: '',
|
||||
reason: ''
|
||||
});
|
||||
|
||||
// История изменений (загружается из блокчейна)
|
||||
const settingsHistory = ref([]);
|
||||
|
||||
// Методы
|
||||
const updateSettings = async () => {
|
||||
if (isUpdating.value) return;
|
||||
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
|
||||
// Здесь будет логика обновления настроек в смарт-контракте
|
||||
// console.log('Обновление настроек:', newSettings.value);
|
||||
|
||||
// Временная логика
|
||||
const change = {
|
||||
id: settingsHistory.value.length + 1,
|
||||
timestamp: Date.now(),
|
||||
reason: newSettings.value.reason,
|
||||
author: '0x' + Math.random().toString(16).substr(2, 40)
|
||||
};
|
||||
|
||||
// Добавляем изменения в историю
|
||||
if (newSettings.value.quorumPercentage && newSettings.value.quorumPercentage !== currentQuorum.value) {
|
||||
change.quorumChange = { from: currentQuorum.value, to: parseInt(newSettings.value.quorumPercentage) };
|
||||
currentQuorum.value = parseInt(newSettings.value.quorumPercentage);
|
||||
}
|
||||
|
||||
if (newSettings.value.votingDelay && newSettings.value.votingDelay !== votingDelay.value) {
|
||||
change.votingDelayChange = { from: votingDelay.value, to: parseInt(newSettings.value.votingDelay) };
|
||||
votingDelay.value = parseInt(newSettings.value.votingDelay);
|
||||
}
|
||||
|
||||
if (newSettings.value.votingPeriod && newSettings.value.votingPeriod !== votingPeriod.value) {
|
||||
change.votingPeriodChange = { from: votingPeriod.value, to: parseInt(newSettings.value.votingPeriod) };
|
||||
votingPeriod.value = parseInt(newSettings.value.votingPeriod);
|
||||
}
|
||||
|
||||
if (newSettings.value.proposalThreshold && newSettings.value.proposalThreshold !== proposalThreshold.value) {
|
||||
change.proposalThresholdChange = { from: proposalThreshold.value, to: parseInt(newSettings.value.proposalThreshold) };
|
||||
proposalThreshold.value = parseInt(newSettings.value.proposalThreshold);
|
||||
}
|
||||
|
||||
settingsHistory.value.unshift(change);
|
||||
|
||||
// Сброс формы
|
||||
newSettings.value = {
|
||||
quorumPercentage: '',
|
||||
votingDelay: '',
|
||||
votingPeriod: '',
|
||||
proposalThreshold: '',
|
||||
reason: ''
|
||||
};
|
||||
|
||||
alert('Настройки успешно обновлены!');
|
||||
|
||||
} catch (error) {
|
||||
// console.error('Ошибка обновления настроек:', error);
|
||||
alert('Ошибка при обновлении настроек');
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
const formatDate = (timestamp) => {
|
||||
return new Date(timestamp).toLocaleString('ru-RU');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quorum-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.current-settings-section,
|
||||
.change-settings-section,
|
||||
.history-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.current-settings-section h2,
|
||||
.change-settings-section h2,
|
||||
.history-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Текущие настройки */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.setting-card {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.setting-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Форма настроек */
|
||||
.settings-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* История изменений */
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.history-header h3 {
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.change-date {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.change-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.change-details p {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.changes-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.change-label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.change-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.change-author {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey-dark);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.history-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.change-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,740 +0,0 @@
|
||||
<!--
|
||||
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="tokens-container">
|
||||
<!-- Заголовок -->
|
||||
<div class="page-header">
|
||||
<div class="header-content">
|
||||
<h1>Управление токенами DLE</h1>
|
||||
<p>Создание предложений для перевода токенов через систему голосования</p>
|
||||
<div v-if="selectedDle" class="dle-info">
|
||||
<span class="dle-name">{{ selectedDle.name }} ({{ selectedDle.symbol }})</span>
|
||||
<span class="dle-address">{{ shortenAddress(selectedDle.dleAddress) }}</span>
|
||||
</div>
|
||||
<div v-else-if="isLoadingDle" class="loading-info">
|
||||
<span>Загрузка данных DLE...</span>
|
||||
</div>
|
||||
<div v-else class="no-dle-info">
|
||||
<span>DLE не выбран</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="close-btn" @click="goBackToBlocks">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Информация о токенах -->
|
||||
<div class="token-info-section">
|
||||
<h2>Информация о токенах</h2>
|
||||
<div class="token-info-grid">
|
||||
<div class="info-card">
|
||||
<h3>Общий запас</h3>
|
||||
<p class="token-amount">{{ totalSupply }} {{ tokenSymbol }}</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Ваш баланс</h3>
|
||||
<p class="token-amount">{{ userBalance }} {{ tokenSymbol }}</p>
|
||||
<p v-if="currentUserAddress" class="user-address">{{ shortenAddress(currentUserAddress) }}</p>
|
||||
<p v-else class="no-wallet">Кошелек не подключен</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>Цена токена</h3>
|
||||
<p class="token-amount">${{ tokenPrice }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Перевод токенов через governance -->
|
||||
<div class="transfer-section">
|
||||
<h2>Перевод токенов через Governance</h2>
|
||||
<p class="section-description">
|
||||
Создайте предложение для перевода токенов через систему голосования.
|
||||
Токены будут переведены от имени DLE после одобрения кворумом.
|
||||
<strong>Важно:</strong> Перевод через governance будет выполнен во всех поддерживаемых сетях DLE.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="createTransferProposal" class="transfer-form">
|
||||
<div class="form-group">
|
||||
<label for="proposal-recipient">Адрес получателя:</label>
|
||||
<input
|
||||
id="proposal-recipient"
|
||||
v-model="proposalData.recipient"
|
||||
type="text"
|
||||
placeholder="0x..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proposal-amount">Количество токенов:</label>
|
||||
<input
|
||||
id="proposal-amount"
|
||||
v-model="proposalData.amount"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
placeholder="0.0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proposal-description">Описание предложения:</label>
|
||||
<textarea
|
||||
id="proposal-description"
|
||||
v-model="proposalData.description"
|
||||
placeholder="Опишите причину перевода токенов..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proposal-duration">Длительность голосования (часы):</label>
|
||||
<input
|
||||
id="proposal-duration"
|
||||
v-model="proposalData.duration"
|
||||
type="number"
|
||||
min="1"
|
||||
max="168"
|
||||
placeholder="24"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" :disabled="isCreatingProposal">
|
||||
{{ isCreatingProposal ? 'Создание предложения...' : 'Создать предложение' }}
|
||||
</button>
|
||||
|
||||
<!-- Статус предложения -->
|
||||
<div v-if="proposalStatus" class="proposal-status">
|
||||
<p class="status-message">{{ proposalStatus }}</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Держатели токенов -->
|
||||
<div class="holders-section">
|
||||
<h2>Держатели токенов</h2>
|
||||
<div v-if="tokenHolders.length === 0" class="empty-state">
|
||||
<p>Нет данных о держателях токенов</p>
|
||||
</div>
|
||||
<div v-else class="holders-list">
|
||||
<div
|
||||
v-for="holder in tokenHolders"
|
||||
:key="holder.address"
|
||||
class="holder-item"
|
||||
>
|
||||
<div class="holder-info">
|
||||
<span class="holder-address">{{ formatAddress(holder.address) }}</span>
|
||||
<span class="holder-balance">{{ holder.balance }} {{ tokenSymbol }}</span>
|
||||
</div>
|
||||
<div class="holder-percentage">
|
||||
{{ ((holder.balance / totalSupply) * 100).toFixed(2) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch, defineProps, defineEmits } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import BaseLayout from '../../components/BaseLayout.vue';
|
||||
import { getTokenBalance, getTotalSupply, getTokenHolders } from '../../services/tokensService.js';
|
||||
import api from '../../api/axios';
|
||||
import { ethers } from 'ethers';
|
||||
import { createTransferTokensProposal } from '../../utils/dle-contract.js';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: Boolean,
|
||||
identities: Array,
|
||||
tokenBalances: Object,
|
||||
isLoadingTokens: Boolean
|
||||
});
|
||||
|
||||
// Определяем emits
|
||||
const emit = defineEmits(['auth-action-completed']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// Получаем адрес DLE из URL
|
||||
const dleAddress = computed(() => {
|
||||
const address = route.query.address;
|
||||
console.log('DLE Address from URL (Tokens):', address);
|
||||
return address;
|
||||
});
|
||||
|
||||
// Функция возврата к блокам управления
|
||||
const goBackToBlocks = () => {
|
||||
if (dleAddress.value) {
|
||||
router.push(`/management/dle-blocks?address=${dleAddress.value}`);
|
||||
} else {
|
||||
router.push('/management');
|
||||
}
|
||||
};
|
||||
|
||||
// Состояние DLE
|
||||
const selectedDle = ref(null);
|
||||
const isLoadingDle = ref(false);
|
||||
|
||||
// Состояние для предложения о переводе токенов через governance
|
||||
const isCreatingProposal = ref(false);
|
||||
const proposalStatus = ref('');
|
||||
|
||||
// Данные токенов (загружаются из блокчейна)
|
||||
const totalSupply = ref(0);
|
||||
const userBalance = ref(0);
|
||||
const deployerBalance = ref(0);
|
||||
const quorumPercentage = ref(0);
|
||||
const tokenPrice = ref(0);
|
||||
|
||||
// Данные для формы
|
||||
const proposalData = ref({
|
||||
recipient: '',
|
||||
amount: '',
|
||||
description: '',
|
||||
duration: 86400, // 24 часа по умолчанию
|
||||
governanceChainId: 11155111, // Sepolia по умолчанию
|
||||
targetChains: [11155111] // Sepolia по умолчанию
|
||||
});
|
||||
|
||||
// Получаем адрес текущего пользователя
|
||||
const currentUserAddress = computed(() => {
|
||||
console.log('Проверяем identities:', props.identities);
|
||||
|
||||
// Получаем адрес из props или из window.ethereum
|
||||
if (props.identities && props.identities.length > 0) {
|
||||
const walletIdentity = props.identities.find(id => id.provider === 'wallet');
|
||||
console.log('Найден wallet identity:', walletIdentity);
|
||||
if (walletIdentity) {
|
||||
return walletIdentity.provider_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: пытаемся получить из window.ethereum
|
||||
if (window.ethereum && window.ethereum.selectedAddress) {
|
||||
console.log('Получаем адрес из window.ethereum:', window.ethereum.selectedAddress);
|
||||
return window.ethereum.selectedAddress;
|
||||
}
|
||||
|
||||
console.log('Адрес пользователя не найден');
|
||||
return null;
|
||||
});
|
||||
|
||||
// Держатели токенов (загружаются из блокчейна)
|
||||
const tokenHolders = ref([]);
|
||||
|
||||
// Функции
|
||||
async function loadDleData() {
|
||||
if (!dleAddress.value) {
|
||||
console.warn('Адрес DLE не указан');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingDle.value = true;
|
||||
try {
|
||||
// Читаем актуальные данные из блокчейна
|
||||
const response = await api.post('/dle-core/read-dle-info', {
|
||||
dleAddress: dleAddress.value
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const blockchainData = response.data.data;
|
||||
selectedDle.value = blockchainData;
|
||||
console.log('Загружены данные DLE из блокчейна:', blockchainData);
|
||||
|
||||
// Загружаем баланс текущего пользователя
|
||||
await loadUserBalance();
|
||||
|
||||
// Загружаем держателей токенов (если есть API)
|
||||
await loadTokenHolders();
|
||||
} else {
|
||||
console.warn('Не удалось прочитать данные из блокчейна для', dleAddress.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
|
||||
} finally {
|
||||
isLoadingDle.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Новая функция для загрузки баланса текущего пользователя
|
||||
async function loadUserBalance() {
|
||||
if (!currentUserAddress.value || !dleAddress.value) {
|
||||
userBalance.value = 0;
|
||||
console.log('Не удается загрузить баланс: нет адреса пользователя или DLE');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Загружаем баланс для пользователя:', currentUserAddress.value);
|
||||
|
||||
const response = await api.post('/blockchain/get-token-balance', {
|
||||
dleAddress: dleAddress.value,
|
||||
account: currentUserAddress.value
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
userBalance.value = parseFloat(response.data.data.balance);
|
||||
console.log('Баланс пользователя загружен:', userBalance.value);
|
||||
} else {
|
||||
console.warn('Не удалось загрузить баланс пользователя:', response.data.error);
|
||||
userBalance.value = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки баланса пользователя:', error);
|
||||
userBalance.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTokenHolders() {
|
||||
try {
|
||||
// Здесь можно добавить загрузку держателей токенов из блокчейна
|
||||
// Пока оставляем пустым
|
||||
tokenHolders.value = [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки держателей токенов:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function shortenAddress(address) {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
// Методы
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||
};
|
||||
|
||||
// Функция создания предложения о переводе токенов через governance
|
||||
const createTransferProposal = async () => {
|
||||
if (isCreatingProposal.value) return;
|
||||
|
||||
try {
|
||||
// Проверяем подключение к кошельку
|
||||
if (!window.ethereum) {
|
||||
alert('Пожалуйста, установите MetaMask или другой Web3 кошелек');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что пользователь подключен
|
||||
if (!currentUserAddress.value) {
|
||||
alert('Пожалуйста, подключите кошелек');
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация данных
|
||||
const recipient = proposalData.value.recipient.trim();
|
||||
const amount = parseFloat(proposalData.value.amount);
|
||||
const description = proposalData.value.description.trim();
|
||||
|
||||
if (!recipient) {
|
||||
alert('Пожалуйста, укажите адрес получателя');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что адрес получателя является корректным Ethereum адресом
|
||||
if (!ethers.isAddress(recipient)) {
|
||||
alert('Пожалуйста, укажите корректный Ethereum адрес получателя');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
alert('Пожалуйста, укажите корректное количество токенов');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
alert('Пожалуйста, укажите описание предложения');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, что получатель не является отправителем
|
||||
if (recipient.toLowerCase() === currentUserAddress.value.toLowerCase()) {
|
||||
alert('Нельзя отправить токены самому себе');
|
||||
return;
|
||||
}
|
||||
|
||||
isCreatingProposal.value = true;
|
||||
proposalStatus.value = 'Создание предложения...';
|
||||
|
||||
// Создаем предложение
|
||||
const result = await createTransferTokensProposal(dleAddress.value, {
|
||||
recipient: recipient,
|
||||
amount: amount,
|
||||
description: description,
|
||||
duration: proposalData.value.duration * 3600, // Конвертируем часы в секунды
|
||||
governanceChainId: proposalData.value.governanceChainId,
|
||||
targetChains: proposalData.value.targetChains
|
||||
});
|
||||
|
||||
proposalStatus.value = 'Предложение создано!';
|
||||
console.log('Предложение о переводе токенов создано:', result);
|
||||
|
||||
// Сброс формы
|
||||
proposalData.value = {
|
||||
recipient: '',
|
||||
amount: '',
|
||||
description: '',
|
||||
duration: 86400,
|
||||
governanceChainId: 11155111,
|
||||
targetChains: [11155111]
|
||||
};
|
||||
|
||||
// Очищаем статус через 5 секунд
|
||||
setTimeout(() => {
|
||||
proposalStatus.value = '';
|
||||
}, 5000);
|
||||
|
||||
alert(`Предложение о переводе токенов создано!\nID предложения: ${result.proposalId}\nХеш транзакции: ${result.txHash}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания предложения о переводе токенов:', error);
|
||||
|
||||
// Очищаем статус предложения
|
||||
proposalStatus.value = '';
|
||||
|
||||
let errorMessage = 'Ошибка создания предложения о переводе токенов';
|
||||
|
||||
if (error.code === 4001) {
|
||||
errorMessage = 'Транзакция отменена пользователем';
|
||||
} else if (error.message && error.message.includes('insufficient funds')) {
|
||||
errorMessage = 'Недостаточно ETH для оплаты газа';
|
||||
} else if (error.message && error.message.includes('execution reverted')) {
|
||||
errorMessage = 'Ошибка выполнения транзакции. Проверьте данные и попробуйте снова';
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
} finally {
|
||||
isCreatingProposal.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Отслеживаем изменения в адресе DLE
|
||||
watch(dleAddress, (newAddress) => {
|
||||
if (newAddress) {
|
||||
loadDleData();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Отслеживаем изменения адреса пользователя
|
||||
watch(currentUserAddress, (newAddress) => {
|
||||
if (newAddress && dleAddress.value) {
|
||||
loadUserBalance();
|
||||
} else {
|
||||
userBalance.value = 0;
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tokens-container {
|
||||
padding: 20px;
|
||||
background-color: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--color-primary);
|
||||
font-size: 2.5rem;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dle-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dle-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dle-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
background: #f8f9fa;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.loading-info,
|
||||
.no-dle-info {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Секции */
|
||||
.token-info-section,
|
||||
.transfer-section,
|
||||
.holders-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.token-info-section h2,
|
||||
.transfer-section h2,
|
||||
.holders-section h2 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Информация о токенах */
|
||||
.token-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border-left: 4px solid var(--color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 15px;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.token-amount {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.user-address {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
color: #666;
|
||||
margin: 5px 0 0 0;
|
||||
background: #f8f9fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.no-wallet {
|
||||
font-size: 0.75rem;
|
||||
color: #dc3545;
|
||||
margin: 5px 0 0 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Формы */
|
||||
.transfer-form {
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--color-grey-dark);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
/* Статус предложения */
|
||||
.proposal-status {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #e8f5e8;
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.proposal-status .status-message {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
/* Описание секции */
|
||||
.section-description {
|
||||
color: var(--color-grey-dark);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn-primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-secondary-dark);
|
||||
}
|
||||
|
||||
/* Состояния */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
color: var(--color-grey-dark);
|
||||
background: #f8f9fa;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 2px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recipient-item {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.holder-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.token-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user