ваше сообщение коммита

This commit is contained in:
2025-09-30 00:23:37 +03:00
parent ca718e3178
commit 4b03951b31
77 changed files with 17161 additions and 7255 deletions

View File

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

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

View File

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

View File

@@ -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);
}
// Если пользователь только что аутентифицировался или сменил аккаунт,
// связываем гостевые сообщения с его аккаунтом

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

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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`],
// });
// Запрашиваем подпись

View 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)"
];

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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