ваше сообщение коммита
This commit is contained in:
@@ -934,12 +934,12 @@ router.get('/check-tokens/:address', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { address } = req.params;
|
const { address } = req.params;
|
||||||
|
|
||||||
// Получаем балансы токенов на всех сетях
|
// Получаем балансы токенов с минимальными балансами
|
||||||
const balances = await authService.getTokenBalances(address);
|
const balances = await authService.getUserTokenBalances(address);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
balances,
|
data: balances,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking token balances:', error);
|
logger.error('Error checking token balances:', error);
|
||||||
|
|||||||
@@ -108,4 +108,300 @@ router.post('/read-dle-info', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Получение поддерживаемых сетей из смарт-контракта
|
||||||
|
router.post('/get-supported-chains', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Адрес DLE обязателен'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Получение поддерживаемых сетей для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем RPC URL для Sepolia
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'RPC URL для Sepolia не найден'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// ABI для проверки поддерживаемых сетей
|
||||||
|
const dleAbi = [
|
||||||
|
"function isChainSupported(uint256 _chainId) external view returns (bool)",
|
||||||
|
"function getCurrentChainId() external view returns (uint256)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Список всех возможных сетей для проверки
|
||||||
|
const allChains = [
|
||||||
|
{ chainId: 1, name: 'Ethereum', description: 'Основная сеть Ethereum' },
|
||||||
|
{ chainId: 137, name: 'Polygon', description: 'Сеть Polygon' },
|
||||||
|
{ chainId: 56, name: 'BSC', description: 'Binance Smart Chain' },
|
||||||
|
{ chainId: 42161, name: 'Arbitrum', description: 'Arbitrum One' },
|
||||||
|
{ chainId: 10, name: 'Optimism', description: 'Optimism' },
|
||||||
|
{ chainId: 8453, name: 'Base', description: 'Base' },
|
||||||
|
{ chainId: 43114, name: 'Avalanche', description: 'Avalanche C-Chain' },
|
||||||
|
{ chainId: 250, name: 'Fantom', description: 'Fantom Opera' },
|
||||||
|
{ chainId: 11155111, name: 'Sepolia', description: 'Ethereum Testnet Sepolia' },
|
||||||
|
{ chainId: 80001, name: 'Mumbai', description: 'Polygon Testnet Mumbai' },
|
||||||
|
{ chainId: 97, name: 'BSC Testnet', description: 'Binance Smart Chain Testnet' },
|
||||||
|
{ chainId: 421613, name: 'Arbitrum Goerli', description: 'Arbitrum Testnet Goerli' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const supportedChains = [];
|
||||||
|
|
||||||
|
// Проверяем каждую сеть через смарт-контракт
|
||||||
|
for (const chain of allChains) {
|
||||||
|
try {
|
||||||
|
const isSupported = await dle.isChainSupported(chain.chainId);
|
||||||
|
if (isSupported) {
|
||||||
|
supportedChains.push(chain);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[Blockchain] Ошибка при проверке сети ${chain.chainId}:`, error.message);
|
||||||
|
// Продолжаем проверку других сетей
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Найдено поддерживаемых сетей: ${supportedChains.length}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
chains: supportedChains,
|
||||||
|
totalCount: supportedChains.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Blockchain] Ошибка при получении поддерживаемых сетей:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при получении поддерживаемых сетей: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение списка всех предложений
|
||||||
|
router.post('/get-proposals', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Адрес DLE обязателен'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Получение списка предложений для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем RPC URL для Sepolia
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'RPC URL для Sepolia не найден'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// ABI для чтения предложений (только читаемые поля)
|
||||||
|
const dleAbi = [
|
||||||
|
"function proposals(uint256) external view returns (uint256 id, string description, uint256 forVotes, uint256 againstVotes, bool executed, uint256 deadline, address initiator, bytes operation)",
|
||||||
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool)",
|
||||||
|
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Получаем события ProposalCreated для определения количества предложений
|
||||||
|
const currentBlock = await provider.getBlockNumber();
|
||||||
|
const fromBlock = Math.max(0, currentBlock - 10000); // Последние 10000 блоков
|
||||||
|
|
||||||
|
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Найдено событий ProposalCreated: ${events.length}`);
|
||||||
|
console.log(`[Blockchain] Диапазон блоков: ${fromBlock} - ${currentBlock}`);
|
||||||
|
|
||||||
|
const proposals = [];
|
||||||
|
|
||||||
|
// Читаем информацию о каждом предложении
|
||||||
|
for (let i = 0; i < events.length; i++) {
|
||||||
|
try {
|
||||||
|
const proposalId = events[i].args.proposalId;
|
||||||
|
console.log(`[Blockchain] Читаем предложение ID: ${proposalId}`);
|
||||||
|
|
||||||
|
// Пробуем несколько раз для новых предложений
|
||||||
|
let proposal, isPassed;
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 3;
|
||||||
|
|
||||||
|
while (retryCount < maxRetries) {
|
||||||
|
try {
|
||||||
|
proposal = await dle.proposals(proposalId);
|
||||||
|
isPassed = await dle.checkProposalResult(proposalId);
|
||||||
|
break; // Успешно прочитали
|
||||||
|
} catch (error) {
|
||||||
|
retryCount++;
|
||||||
|
console.log(`[Blockchain] Попытка ${retryCount} чтения предложения ${proposalId} не удалась:`, error.message);
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000)); // Ждем 2 секунды
|
||||||
|
} else {
|
||||||
|
throw error; // Превышено количество попыток
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// governanceChainId не сохраняется в предложении, используем текущую цепочку
|
||||||
|
const governanceChainId = 11155111; // Sepolia chain ID
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Данные предложения ${proposalId}:`, {
|
||||||
|
id: Number(proposal.id),
|
||||||
|
description: proposal.description,
|
||||||
|
forVotes: Number(proposal.forVotes),
|
||||||
|
againstVotes: Number(proposal.againstVotes),
|
||||||
|
executed: proposal.executed,
|
||||||
|
deadline: Number(proposal.deadline),
|
||||||
|
initiator: proposal.initiator,
|
||||||
|
operation: proposal.operation,
|
||||||
|
governanceChainId: Number(governanceChainId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const proposalInfo = {
|
||||||
|
id: Number(proposal.id),
|
||||||
|
description: proposal.description,
|
||||||
|
forVotes: Number(proposal.forVotes),
|
||||||
|
againstVotes: Number(proposal.againstVotes),
|
||||||
|
executed: proposal.executed,
|
||||||
|
deadline: Number(proposal.deadline),
|
||||||
|
initiator: proposal.initiator,
|
||||||
|
operation: proposal.operation,
|
||||||
|
governanceChainId: Number(governanceChainId),
|
||||||
|
isPassed: isPassed,
|
||||||
|
blockNumber: events[i].blockNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
proposals.push(proposalInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[Blockchain] Ошибка при чтении предложения ${i}:`, error.message);
|
||||||
|
|
||||||
|
// Если это ошибка декодирования, возможно предложение еще не полностью записано
|
||||||
|
if (error.message.includes('could not decode result data')) {
|
||||||
|
console.log(`[Blockchain] Предложение ${i} еще не полностью синхронизировано, пропускаем`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Продолжаем с следующим предложением
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортируем по ID предложения (новые сверху)
|
||||||
|
proposals.sort((a, b) => b.id - a.id);
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Найдено предложений: ${proposals.length}`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
proposals: proposals,
|
||||||
|
totalCount: proposals.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Blockchain] Ошибка при получении списка предложений:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при получении списка предложений: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение информации о предложении
|
||||||
|
router.post('/get-proposal-info', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { dleAddress, proposalId } = req.body;
|
||||||
|
|
||||||
|
if (!dleAddress || proposalId === undefined) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Все поля обязательны: dleAddress, proposalId'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Получение информации о предложении ${proposalId} в DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем RPC URL для Sepolia
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(11155111);
|
||||||
|
if (!rpcUrl) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'RPC URL для Sepolia не найден'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// ABI для чтения информации о предложении
|
||||||
|
const dleAbi = [
|
||||||
|
"function proposals(uint256) external view returns (tuple(string description, uint256 duration, bytes operation, uint256 governanceChainId, uint256 startTime, bool executed, uint256 forVotes, uint256 againstVotes))",
|
||||||
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
// Читаем информацию о предложении
|
||||||
|
const proposal = await dle.proposals(proposalId);
|
||||||
|
const isPassed = await dle.checkProposalResult(proposalId);
|
||||||
|
|
||||||
|
// governanceChainId не сохраняется в предложении, используем текущую цепочку
|
||||||
|
const governanceChainId = 11155111; // Sepolia chain ID
|
||||||
|
|
||||||
|
const proposalInfo = {
|
||||||
|
description: proposal.description,
|
||||||
|
duration: Number(proposal.duration),
|
||||||
|
operation: proposal.operation,
|
||||||
|
governanceChainId: Number(proposal.governanceChainId),
|
||||||
|
startTime: Number(proposal.startTime),
|
||||||
|
executed: proposal.executed,
|
||||||
|
forVotes: Number(proposal.forVotes),
|
||||||
|
againstVotes: Number(proposal.againstVotes),
|
||||||
|
isPassed: isPassed
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[Blockchain] Информация о предложении получена:`, proposalInfo);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: proposalInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Blockchain] Ошибка при получении информации о предложении:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при получении информации о предложении: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Импортируем WebSocket функции из wsHub
|
||||||
|
const { broadcastProposalCreated, broadcastProposalVoted, broadcastProposalExecuted } = require('../wsHub');
|
||||||
|
|
||||||
|
// Экспортируем router как основной экспорт
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -71,9 +71,9 @@ router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) =>
|
|||||||
/**
|
/**
|
||||||
* @route GET /api/dle-v2
|
* @route GET /api/dle-v2
|
||||||
* @desc Получить список всех DLE v2
|
* @desc Получить список всех DLE v2
|
||||||
* @access Private (только для авторизованных пользователей)
|
* @access Public (доступно всем пользователям)
|
||||||
*/
|
*/
|
||||||
router.get('/', auth.requireAuth, async (req, res, next) => {
|
router.get('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const dles = dleV2Service.getAllDLEs();
|
const dles = dleV2Service.getAllDLEs();
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ async function initServices() {
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
initWSS(server);
|
initWSS(server);
|
||||||
|
|
||||||
|
// WebSocket уже инициализирован в wsHub.js
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
await initDbPool(); // Дождаться пересоздания пула!
|
await initDbPool(); // Дождаться пересоздания пула!
|
||||||
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
|
await seedAIAssistantSettings(); // Инициализация ассистента после загрузки модели Ollama
|
||||||
|
|||||||
@@ -890,6 +890,7 @@ class AuthService {
|
|||||||
tokenName: token.name,
|
tokenName: token.name,
|
||||||
symbol: token.symbol || '',
|
symbol: token.symbol || '',
|
||||||
balance,
|
balance,
|
||||||
|
minBalance: token.min_balance,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -62,7 +62,14 @@ async function upsertAuthToken(token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAuthToken(address, network) {
|
async function deleteAuthToken(address, network) {
|
||||||
|
console.log(`[AuthTokenService] deleteAuthToken: address=${address}, network=${network}`);
|
||||||
|
try {
|
||||||
await encryptedDb.deleteData('auth_tokens', { address, network });
|
await encryptedDb.deleteData('auth_tokens', { address, network });
|
||||||
|
console.log(`[AuthTokenService] Токен успешно удален`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[AuthTokenService] Ошибка при удалении токена:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };
|
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };
|
||||||
@@ -323,48 +323,73 @@ class EncryptedDataService {
|
|||||||
*/
|
*/
|
||||||
async deleteData(tableName, conditions) {
|
async deleteData(tableName, conditions) {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[EncryptedDataService] deleteData: tableName=${tableName}, conditions=`, conditions);
|
||||||
|
|
||||||
|
// Проверяем, включено ли шифрование
|
||||||
|
if (!this.isEncryptionEnabled) {
|
||||||
|
return await this.executeUnencryptedQuery(tableName, conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о колонках
|
||||||
|
const { rows: columns } = await db.getQuery()(`
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND table_schema = 'public'
|
||||||
|
ORDER BY ordinal_position
|
||||||
|
`, [tableName]);
|
||||||
|
|
||||||
|
console.log(`[EncryptedDataService] Columns for ${tableName}:`, columns.map(c => c.column_name));
|
||||||
|
|
||||||
// Функция для заключения зарезервированных слов в кавычки
|
// Функция для заключения зарезервированных слов в кавычки
|
||||||
const quoteReservedWord = (word) => {
|
const quoteReservedWord = (word) => {
|
||||||
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
|
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
|
||||||
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
|
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Проверяем, включено ли шифрование
|
|
||||||
if (!this.isEncryptionEnabled) {
|
|
||||||
let query = `DELETE FROM ${tableName}`;
|
let query = `DELETE FROM ${tableName}`;
|
||||||
const params = [];
|
const params = [];
|
||||||
let paramIndex = 1;
|
let paramIndex = 1;
|
||||||
|
|
||||||
if (Object.keys(conditions).length > 0) {
|
if (Object.keys(conditions).length > 0) {
|
||||||
const whereClause = Object.keys(conditions)
|
const whereClause = Object.keys(conditions)
|
||||||
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
|
.map((key, index) => {
|
||||||
|
const value = conditions[key];
|
||||||
|
|
||||||
|
// Проверяем, есть ли зашифрованная версия колонки
|
||||||
|
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
|
||||||
|
|
||||||
|
if (encryptedColumn) {
|
||||||
|
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
|
||||||
|
// Ключ шифрования всегда первый параметр ($1), затем значения
|
||||||
|
return `${key}_encrypted = encrypt_text($${index + 2}, $1)`;
|
||||||
|
} else {
|
||||||
|
// Для незашифрованных колонок используем обычное сравнение
|
||||||
|
const columnName = quoteReservedWord(key);
|
||||||
|
return `${columnName} = $${index + 1}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(' AND ');
|
.join(' AND ');
|
||||||
query += ` WHERE ${whereClause}`;
|
query += ` WHERE ${whereClause}`;
|
||||||
params.push(...Object.values(conditions));
|
|
||||||
|
// Добавляем параметры
|
||||||
|
const paramsToAdd = Object.values(conditions);
|
||||||
|
params.push(...paramsToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows } = await db.getQuery()(query, params);
|
// Добавляем ключ шифрования в начало, если есть зашифрованные поля
|
||||||
return rows;
|
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
|
||||||
|
if (hasEncryptedFields) {
|
||||||
|
params.unshift(this.encryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для зашифрованных таблиц - пока используем обычный DELETE
|
console.log(`[EncryptedDataService] DELETE query: ${query}`);
|
||||||
// TODO: Добавить логику для зашифрованных условий WHERE
|
console.log(`[EncryptedDataService] DELETE params:`, params);
|
||||||
let query = `DELETE FROM ${tableName}`;
|
|
||||||
const params = [];
|
|
||||||
let paramIndex = 1;
|
|
||||||
|
|
||||||
if (Object.keys(conditions).length > 0) {
|
|
||||||
const whereClause = Object.keys(conditions)
|
|
||||||
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
|
|
||||||
.join(' AND ');
|
|
||||||
query += ` WHERE ${whereClause}`;
|
|
||||||
params.push(...Object.values(conditions));
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.getQuery()(query, params);
|
const result = await db.getQuery()(query, params);
|
||||||
return result.rows;
|
return result.rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
|
console.error(`[EncryptedDataService] ❌ Ошибка удаления данных из ${tableName}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,6 +313,58 @@ function broadcastAIStatus(status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blockchain уведомления
|
||||||
|
function broadcastProposalCreated(dleAddress, proposalId, txHash) {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'proposal_created',
|
||||||
|
data: {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId,
|
||||||
|
txHash: txHash
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcastToAllClients(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastProposalVoted(dleAddress, proposalId, support, txHash) {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'proposal_voted',
|
||||||
|
data: {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId,
|
||||||
|
support: support,
|
||||||
|
txHash: txHash
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcastToAllClients(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastProposalExecuted(dleAddress, proposalId, txHash) {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'proposal_executed',
|
||||||
|
data: {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId,
|
||||||
|
txHash: txHash
|
||||||
|
},
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcastToAllClients(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function broadcastToAllClients(message) {
|
||||||
|
wss.clients.forEach((client) => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initWSS,
|
initWSS,
|
||||||
broadcastContactsUpdate,
|
broadcastContactsUpdate,
|
||||||
@@ -323,6 +375,9 @@ module.exports = {
|
|||||||
broadcastTableRelationsUpdate,
|
broadcastTableRelationsUpdate,
|
||||||
broadcastTagsUpdate,
|
broadcastTagsUpdate,
|
||||||
broadcastAIStatus,
|
broadcastAIStatus,
|
||||||
|
broadcastProposalCreated,
|
||||||
|
broadcastProposalVoted,
|
||||||
|
broadcastProposalExecuted,
|
||||||
getConnectedUsers,
|
getConnectedUsers,
|
||||||
getStats
|
getStats
|
||||||
};
|
};
|
||||||
@@ -12,11 +12,25 @@ server {
|
|||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Блокировка очень старых браузеров
|
# Блокировка старых браузеров и подозрительных User-Agent
|
||||||
if ($http_user_agent ~* "MSIE [1-8]\.") {
|
if ($http_user_agent ~* "Chrome/[1-7][0-9]\.") {
|
||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($http_user_agent ~* "Safari/[1-5][0-9][0-9]\.") {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Блокировка подозрительных поддоменов
|
||||||
|
if ($host !~* "^(hb3-accelerator\.com|www\.hb3-accelerator\.com|localhost|127\.0\.0\.1)$") {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Блокировка сканирования резервных копий и архивов
|
||||||
|
if ($request_uri ~* "(backup|backups|bak|old|restore|www\.tar|website\.tar|\.tar\.gz|\.gz|\.sql\.tar|public_html\.tar|sftp-config\.json)") {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
# Блокировка опасных файлов (НЕ блокируем .js, .css)
|
# Блокировка опасных файлов (НЕ блокируем .js, .css)
|
||||||
if ($request_uri ~* "\.(php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|bak|old|tmp|temp|swp|swo|~)$") {
|
if ($request_uri ~* "\.(php|asp|aspx|jsp|cgi|pl|py|sh|bash|exe|bat|cmd|com|pif|scr|vbs|vbe|jar|war|ear|dll|so|dylib|bin|sys|ini|log|bak|old|tmp|temp|swp|swo|~)$") {
|
||||||
return 404;
|
return 404;
|
||||||
@@ -37,6 +51,16 @@ server {
|
|||||||
return 403;
|
return 403;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Блокировка HEAD запросов к подозрительным файлам
|
||||||
|
if ($request_method = "HEAD" && $request_uri ~* "(backup|backups|bak|old|restore|\.tar|\.gz|\.sql|config\.js|sftp-config\.json)") {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Блокировка всех запросов к конфигурационным файлам
|
||||||
|
if ($request_uri ~* "(config\.js|sftp-config\.json|\.config\.|\.conf\.|\.ini\.|\.env\.|\.json\.)") {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
# Основной location
|
# Основной location
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html =404;
|
try_files $uri $uri/ /index.html =404;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits } from 'vue';
|
import { ref, onMounted, watch, onBeforeUnmount, defineProps, defineEmits, provide, computed } from 'vue';
|
||||||
import { useAuthContext } from '../composables/useAuth';
|
import { useAuthContext } from '../composables/useAuth';
|
||||||
import { useAuthFlow } from '../composables/useAuthFlow';
|
import { useAuthFlow } from '../composables/useAuthFlow';
|
||||||
import { useNotifications } from '../composables/useNotifications';
|
import { useNotifications } from '../composables/useNotifications';
|
||||||
@@ -79,6 +79,12 @@ const props = defineProps({
|
|||||||
// Определяем emits
|
// Определяем emits
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
const emit = defineEmits(['auth-action-completed']);
|
||||||
|
|
||||||
|
// Предоставляем данные дочерним компонентам через provide/inject
|
||||||
|
provide('isAuthenticated', computed(() => props.isAuthenticated));
|
||||||
|
provide('identities', computed(() => props.identities));
|
||||||
|
provide('tokenBalances', computed(() => props.tokenBalances));
|
||||||
|
provide('isLoadingTokens', computed(() => props.isLoadingTokens));
|
||||||
|
|
||||||
// Callback после успешной аутентификации/привязки через Email/Telegram
|
// Callback после успешной аутентификации/привязки через Email/Telegram
|
||||||
const handleAuthFlowSuccess = (authType) => {
|
const handleAuthFlowSuccess = (authType) => {
|
||||||
// console.log(`[BaseLayout] Auth flow success: ${authType}`);
|
// console.log(`[BaseLayout] Auth flow success: ${authType}`);
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ const checkTokenBalances = async (address) => {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(`/auth/check-tokens/${address}`);
|
const response = await axios.get(`/auth/check-tokens/${address}`);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
tokenBalances.value = response.data.balances;
|
tokenBalances.value = response.data.data;
|
||||||
return response.data.balances;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -140,8 +140,8 @@ const updateAuth = async ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
|
// Если аутентификация через кошелек, проверяем баланс токенов
|
||||||
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
|
if (authenticated && newAuthType === 'wallet' && newAddress) {
|
||||||
await checkTokenBalances(newAddress);
|
await checkTokenBalances(newAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +474,7 @@ const authApi = {
|
|||||||
updateConnectionDisplay,
|
updateConnectionDisplay,
|
||||||
linkIdentity,
|
linkIdentity,
|
||||||
deleteIdentity,
|
deleteIdentity,
|
||||||
|
checkTokenBalances,
|
||||||
};
|
};
|
||||||
|
|
||||||
// === PROVIDE/INJECT HELPERS ===
|
// === PROVIDE/INJECT HELPERS ===
|
||||||
|
|||||||
@@ -200,74 +200,63 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/management',
|
path: '/management',
|
||||||
name: 'management',
|
name: 'management',
|
||||||
component: () => import('../views/ManagementView.vue'),
|
component: () => import('../views/ManagementView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/dle',
|
path: '/management/dle',
|
||||||
name: 'management-dle',
|
name: 'management-dle',
|
||||||
component: () => import('../views/smartcontracts/DleModulesView.vue'),
|
component: () => import('../views/smartcontracts/DleModulesView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/dle-management',
|
path: '/management/dle-management',
|
||||||
name: 'management-dle-management',
|
name: 'management-dle-management',
|
||||||
component: () => import('../views/smartcontracts/DleModulesView.vue'),
|
component: () => import('../views/smartcontracts/DleModulesView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/proposals',
|
path: '/management/proposals',
|
||||||
name: 'management-proposals',
|
name: 'management-proposals',
|
||||||
component: () => import('../views/smartcontracts/DleProposalsView.vue'),
|
component: () => import('../views/smartcontracts/DleProposalsView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/tokens',
|
path: '/management/tokens',
|
||||||
name: 'management-tokens',
|
name: 'management-tokens',
|
||||||
component: () => import('../views/smartcontracts/TokensView.vue'),
|
component: () => import('../views/smartcontracts/TokensView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/quorum',
|
path: '/management/quorum',
|
||||||
name: 'management-quorum',
|
name: 'management-quorum',
|
||||||
component: () => import('../views/smartcontracts/QuorumView.vue'),
|
component: () => import('../views/smartcontracts/QuorumView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/modules',
|
path: '/management/modules',
|
||||||
name: 'management-modules',
|
name: 'management-modules',
|
||||||
component: () => import('../views/smartcontracts/DleModulesView.vue'),
|
component: () => import('../views/smartcontracts/DleModulesView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/management/multisig',
|
|
||||||
name: 'management-multisig',
|
|
||||||
component: () => import('../views/smartcontracts/DleMultisigView.vue'),
|
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// path: '/management/multisig',
|
||||||
|
// name: 'management-multisig',
|
||||||
|
// component: () => import('../views/smartcontracts/DleMultisigView.vue'),
|
||||||
|
// meta: { requiresAuth: true }
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
path: '/management/treasury',
|
path: '/management/treasury',
|
||||||
name: 'management-treasury',
|
name: 'management-treasury',
|
||||||
component: () => import('../views/smartcontracts/TreasuryView.vue'),
|
component: () => import('../views/smartcontracts/TreasuryView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/analytics',
|
path: '/management/analytics',
|
||||||
name: 'management-analytics',
|
name: 'management-analytics',
|
||||||
component: () => import('../views/smartcontracts/AnalyticsView.vue'),
|
component: () => import('../views/smartcontracts/AnalyticsView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/history',
|
path: '/management/history',
|
||||||
name: 'management-history',
|
name: 'management-history',
|
||||||
component: () => import('../views/smartcontracts/HistoryView.vue'),
|
component: () => import('../views/smartcontracts/HistoryView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/management/settings',
|
path: '/management/settings',
|
||||||
name: 'management-settings',
|
name: 'management-settings',
|
||||||
component: () => import('../views/smartcontracts/SettingsView.vue'),
|
component: () => import('../views/smartcontracts/SettingsView.vue')
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -292,10 +281,12 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
if (response.data.authenticated) {
|
if (response.data.authenticated) {
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
next('/login');
|
// Перенаправляем на главную страницу, где есть форма аутентификации
|
||||||
|
next({ name: 'home' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next('/login');
|
// При ошибке также перенаправляем на главную
|
||||||
|
next({ name: 'home' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
|
|||||||
467
frontend/src/utils/dle-contract.js
Normal file
467
frontend/src/utils/dle-contract.js
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить подключение к браузерному кошельку
|
||||||
|
* @returns {Promise<Object>} - Информация о подключенном кошельке
|
||||||
|
*/
|
||||||
|
export async function checkWalletConnection() {
|
||||||
|
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();
|
||||||
|
const address = await signer.getAddress();
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
|
||||||
|
return {
|
||||||
|
connected: true,
|
||||||
|
address: address,
|
||||||
|
chainId: Number(network.chainId),
|
||||||
|
provider: window.ethereum.isMetaMask ? 'MetaMask' : 'Other Wallet'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка подключения к кошельку:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Утилита для работы с реальными функциями смарт-контракта DLE
|
||||||
|
* Используется только система голосования (proposals)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить информацию о DLE из блокчейна
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Object>} - Информация о DLE
|
||||||
|
*/
|
||||||
|
export async function getDLEInfo(dleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/api/blockchain/read-dle-info', {
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось прочитать данные из блокчейна');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения информации о DLE:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает данные DLE из блокчейна (алиас для getDLEInfo)
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Object>} - Данные DLE из блокчейна
|
||||||
|
*/
|
||||||
|
export async function loadDleDataFromBlockchain(dleAddress) {
|
||||||
|
return getDLEInfo(dleAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создать новое предложение для голосования через браузерный кошелек
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {Object} proposalData - Данные предложения
|
||||||
|
* @returns {Promise<Object>} - Результат создания
|
||||||
|
*/
|
||||||
|
export async function createProposal(dleAddress, proposalData) {
|
||||||
|
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 dleAbi = [
|
||||||
|
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId) external returns (uint256)"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
// Создаем предложение
|
||||||
|
const tx = await dle.createProposal(
|
||||||
|
proposalData.description,
|
||||||
|
proposalData.duration,
|
||||||
|
proposalData.operation,
|
||||||
|
proposalData.governanceChainId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ждем подтверждения транзакции
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
|
||||||
|
console.log('Предложение создано, tx hash:', tx.hash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
proposalId: receipt.logs[0]?.topics[1] || '0', // Извлекаем ID предложения из события
|
||||||
|
txHash: tx.hash,
|
||||||
|
blockNumber: receipt.blockNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка создания предложения:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Голосовать за предложение через браузерный кошелек
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @param {boolean} support - Поддержка предложения
|
||||||
|
* @returns {Promise<Object>} - Результат голосования
|
||||||
|
*/
|
||||||
|
export async function voteForProposal(dleAddress, proposalId, support) {
|
||||||
|
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 dleAbi = [
|
||||||
|
"function vote(uint256 _proposalId, bool _support) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
// Голосуем за предложение
|
||||||
|
const tx = await dle.vote(proposalId, support);
|
||||||
|
|
||||||
|
// Ждем подтверждения транзакции
|
||||||
|
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 {number} proposalId - ID предложения
|
||||||
|
* @returns {Promise<Object>} - Результат исполнения
|
||||||
|
*/
|
||||||
|
export async function executeProposal(dleAddress, proposalId) {
|
||||||
|
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 dleAbi = [
|
||||||
|
"function executeProposal(uint256 _proposalId) external"
|
||||||
|
];
|
||||||
|
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||||
|
|
||||||
|
// Исполняем предложение
|
||||||
|
const tx = await dle.executeProposal(proposalId);
|
||||||
|
|
||||||
|
// Ждем подтверждения транзакции
|
||||||
|
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} moduleId - ID модуля
|
||||||
|
* @param {string} moduleAddress - Адрес модуля
|
||||||
|
* @returns {Promise<Object>} - Результат добавления
|
||||||
|
*/
|
||||||
|
export async function addModule(dleAddress, moduleId, moduleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/blockchain/add-module', {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
moduleId: moduleId,
|
||||||
|
moduleAddress: moduleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось добавить модуль');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка добавления модуля:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить модуль
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} moduleId - ID модуля
|
||||||
|
* @returns {Promise<Object>} - Результат удаления
|
||||||
|
*/
|
||||||
|
export async function removeModule(dleAddress, moduleId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/blockchain/remove-module', {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
moduleId: moduleId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось удалить модуль');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка удаления модуля:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить, активен ли модуль
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {string} moduleId - ID модуля
|
||||||
|
* @returns {Promise<boolean>} - Активен ли модуль
|
||||||
|
*/
|
||||||
|
export async function isModuleActive(dleAddress, moduleId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/blockchain/is-module-active', {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
moduleId: moduleId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.isActive;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось проверить активность модуля');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки активности модуля:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить, поддерживается ли цепочка
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} chainId - ID цепочки
|
||||||
|
* @returns {Promise<boolean>} - Поддерживается ли цепочка
|
||||||
|
*/
|
||||||
|
export async function isChainSupported(dleAddress, chainId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/blockchain/is-chain-supported', {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
chainId: chainId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.isSupported;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось проверить поддержку цепочки');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки поддержки цепочки:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить текущий ID цепочки
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<number>} - Текущий ID цепочки
|
||||||
|
*/
|
||||||
|
export async function getCurrentChainId(dleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/blockchain/get-current-chain-id', {
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.chainId;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось получить текущий ID цепочки');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения текущего ID цепочки:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить результат предложения
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @param {number} proposalId - ID предложения
|
||||||
|
* @returns {Promise<Object>} - Результат проверки
|
||||||
|
*/
|
||||||
|
export async function checkProposalResult(dleAddress, proposalId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/blockchain/check-proposal-result', {
|
||||||
|
dleAddress: dleAddress,
|
||||||
|
proposalId: proposalId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось проверить результат предложения');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка проверки результата предложения:', error);
|
||||||
|
return { passed: false, quorumReached: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заглушки для совместимости с существующими компонентами
|
||||||
|
// Эти функции не существуют в реальном контракте, но используются в UI
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает предложения DLE из блокчейна
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Array>} - Список предложений
|
||||||
|
*/
|
||||||
|
export async function loadProposals(dleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/api/blockchain/get-proposals', {
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.proposals;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось загрузить предложения');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки предложений:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает держателей токенов DLE (заглушка для UI)
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Array>} - Список держателей токенов
|
||||||
|
*/
|
||||||
|
export async function loadTokenHolders(dleAddress) {
|
||||||
|
try {
|
||||||
|
// В реальности нужно сканировать события Transfer из блокчейна
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки держателей токенов:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает историю операций DLE (заглушка для UI)
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Array>} - Список операций
|
||||||
|
*/
|
||||||
|
export async function loadHistory(dleAddress) {
|
||||||
|
try {
|
||||||
|
// В реальности нужно читать все события из блокчейна
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки истории:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает активы казны DLE (заглушка для UI)
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Array>} - Список активов
|
||||||
|
*/
|
||||||
|
export async function loadTreasuryAssets(dleAddress) {
|
||||||
|
try {
|
||||||
|
// В реальности нужно читать балансы токенов из блокчейна
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки активов казны:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загружает аналитику DLE (заглушка для UI)
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Object>} - Данные аналитики
|
||||||
|
*/
|
||||||
|
export async function loadAnalytics(dleAddress) {
|
||||||
|
try {
|
||||||
|
// В реальности нужно агрегировать данные из блокчейна
|
||||||
|
return {
|
||||||
|
topParticipants: [],
|
||||||
|
totalSupply: 0,
|
||||||
|
participantCount: 0,
|
||||||
|
activeProposals: 0
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки аналитики:', error);
|
||||||
|
return {
|
||||||
|
topParticipants: [],
|
||||||
|
totalSupply: 0,
|
||||||
|
participantCount: 0,
|
||||||
|
activeProposals: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить поддерживаемые цепочки из смарт-контракта
|
||||||
|
* @param {string} dleAddress - Адрес DLE контракта
|
||||||
|
* @returns {Promise<Array>} - Список поддерживаемых цепочек
|
||||||
|
*/
|
||||||
|
export async function getSupportedChains(dleAddress) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/api/blockchain/get-supported-chains', {
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
return response.data.data.chains;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.message || 'Не удалось получить поддерживаемые цепочки');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения поддерживаемых цепочек:', error);
|
||||||
|
// Возвращаем пустой массив если API недоступен
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
135
frontend/src/utils/websocket.js
Normal file
135
frontend/src/utils/websocket.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket клиент для автоматического обновления данных
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WebSocketClient {
|
||||||
|
constructor() {
|
||||||
|
this.ws = null;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.maxReconnectAttempts = 5;
|
||||||
|
this.reconnectDelay = 1000;
|
||||||
|
this.listeners = new Map();
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket('ws://localhost:8000/ws');
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
console.log('[WebSocket] Подключение установлено');
|
||||||
|
this.isConnected = true;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('[WebSocket] Получено сообщение:', data);
|
||||||
|
|
||||||
|
// Вызываем все зарегистрированные обработчики для этого события
|
||||||
|
if (this.listeners.has(data.type)) {
|
||||||
|
this.listeners.get(data.type).forEach(callback => {
|
||||||
|
callback(data.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WebSocket] Ошибка парсинга сообщения:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
console.log('[WebSocket] Соединение закрыто');
|
||||||
|
this.isConnected = false;
|
||||||
|
this.scheduleReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('[WebSocket] Ошибка:', error);
|
||||||
|
this.isConnected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WebSocket] Ошибка подключения:', error);
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleReconnect() {
|
||||||
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
console.log(`[WebSocket] Попытка переподключения ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.connect();
|
||||||
|
}, this.reconnectDelay * this.reconnectAttempts);
|
||||||
|
} else {
|
||||||
|
console.error('[WebSocket] Превышено максимальное количество попыток переподключения');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрация обработчика события
|
||||||
|
on(event, callback) {
|
||||||
|
if (!this.listeners.has(event)) {
|
||||||
|
this.listeners.set(event, []);
|
||||||
|
}
|
||||||
|
this.listeners.get(event).push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаление обработчика события
|
||||||
|
off(event, callback) {
|
||||||
|
if (this.listeners.has(event)) {
|
||||||
|
const callbacks = this.listeners.get(event);
|
||||||
|
const index = callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправка сообщения на сервер
|
||||||
|
send(event, data) {
|
||||||
|
if (this.ws && this.isConnected) {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
event: event,
|
||||||
|
data: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
this.ws.send(message);
|
||||||
|
} else {
|
||||||
|
console.warn('[WebSocket] Не удалось отправить сообщение: соединение не установлено');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подписка на обновления предложений для конкретного DLE
|
||||||
|
subscribeToProposals(dleAddress) {
|
||||||
|
this.send('subscribe', {
|
||||||
|
type: 'proposals',
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отписка от обновлений предложений
|
||||||
|
unsubscribeFromProposals(dleAddress) {
|
||||||
|
this.send('unsubscribe', {
|
||||||
|
type: 'proposals',
|
||||||
|
dleAddress: dleAddress
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем глобальный экземпляр WebSocket клиента
|
||||||
|
const wsClient = new WebSocketClient();
|
||||||
|
|
||||||
|
// Автоматически подключаемся при импорте модуля
|
||||||
|
wsClient.connect();
|
||||||
|
|
||||||
|
export default wsClient;
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
<div class="management-block">
|
<div class="management-block">
|
||||||
<h3>Мультиподпись</h3>
|
<h3>Мультиподпись</h3>
|
||||||
<p>Управление мультиподписью</p>
|
<p>Управление мультиподписью</p>
|
||||||
<button class="details-btn" @click="openMultisigWithDle">Подробнее</button>
|
<!-- Мультиподпись удалена - используется только голосование -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,26 +241,40 @@ const openSettings = () => {
|
|||||||
async function loadDeployedDles() {
|
async function loadDeployedDles() {
|
||||||
try {
|
try {
|
||||||
isLoadingDles.value = true;
|
isLoadingDles.value = true;
|
||||||
|
console.log('[ManagementView] Начинаем загрузку DLE...');
|
||||||
|
|
||||||
// Сначала получаем список DLE из API
|
// Сначала получаем список DLE из API
|
||||||
const response = await api.get('/dle-v2');
|
const response = await api.get('/dle-v2');
|
||||||
|
console.log('[ManagementView] Ответ от API /dle-v2:', response.data);
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
const dlesFromApi = response.data.data || [];
|
const dlesFromApi = response.data.data || [];
|
||||||
|
console.log('[ManagementView] DLE из API:', dlesFromApi);
|
||||||
|
|
||||||
|
if (dlesFromApi.length === 0) {
|
||||||
|
console.log('[ManagementView] Нет DLE в API, показываем пустой список');
|
||||||
|
deployedDles.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Для каждого DLE читаем актуальные данные из блокчейна
|
// Для каждого DLE читаем актуальные данные из блокчейна
|
||||||
const dlesWithBlockchainData = await Promise.all(
|
const dlesWithBlockchainData = await Promise.all(
|
||||||
dlesFromApi.map(async (dle) => {
|
dlesFromApi.map(async (dle) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[ManagementView] Читаем данные из блокчейна для ${dle.dleAddress}`);
|
||||||
|
|
||||||
// Читаем данные из блокчейна
|
// Читаем данные из блокчейна
|
||||||
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
|
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
|
||||||
dleAddress: dle.dleAddress
|
dleAddress: dle.dleAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[ManagementView] Ответ от блокчейна для ${dle.dleAddress}:`, blockchainResponse.data);
|
||||||
|
|
||||||
if (blockchainResponse.data.success) {
|
if (blockchainResponse.data.success) {
|
||||||
const blockchainData = blockchainResponse.data.data;
|
const blockchainData = blockchainResponse.data.data;
|
||||||
|
|
||||||
// Объединяем данные из API с данными из блокчейна
|
// Объединяем данные из API с данными из блокчейна
|
||||||
return {
|
const combinedDle = {
|
||||||
...dle,
|
...dle,
|
||||||
// Данные из блокчейна (приоритет)
|
// Данные из блокчейна (приоритет)
|
||||||
name: blockchainData.name || dle.name,
|
name: blockchainData.name || dle.name,
|
||||||
@@ -277,25 +291,28 @@ async function loadDeployedDles() {
|
|||||||
// Количество участников (держателей токенов)
|
// Количество участников (держателей токенов)
|
||||||
participantCount: blockchainData.participantCount || 0
|
participantCount: blockchainData.participantCount || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(`[ManagementView] Объединенные данные для ${dle.dleAddress}:`, combinedDle);
|
||||||
|
return combinedDle;
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Не удалось прочитать данные из блокчейна для ${dle.dleAddress}`);
|
console.warn(`[ManagementView] Не удалось прочитать данные из блокчейна для ${dle.dleAddress}:`, blockchainResponse.data.error);
|
||||||
return dle;
|
return dle;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`Ошибка при чтении данных из блокчейна для ${dle.dleAddress}:`, error);
|
console.warn(`[ManagementView] Ошибка при чтении данных из блокчейна для ${dle.dleAddress}:`, error);
|
||||||
return dle;
|
return dle;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
deployedDles.value = dlesWithBlockchainData;
|
deployedDles.value = dlesWithBlockchainData;
|
||||||
console.log('Загружены деплоированные DLE с данными из блокчейна:', deployedDles.value);
|
console.log('[ManagementView] Итоговый список DLE:', deployedDles.value);
|
||||||
} else {
|
} else {
|
||||||
console.error('Ошибка при загрузке DLE:', response.data.message);
|
console.error('[ManagementView] Ошибка при загрузке DLE:', response.data.message);
|
||||||
deployedDles.value = [];
|
deployedDles.value = [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при загрузке DLE:', error);
|
console.error('[ManagementView] Ошибка при загрузке DLE:', error);
|
||||||
deployedDles.value = [];
|
deployedDles.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingDles.value = false;
|
isLoadingDles.value = false;
|
||||||
@@ -322,9 +339,9 @@ function selectDle(dle) {
|
|||||||
console.log('Выбран DLE:', dle);
|
console.log('Выбран DLE:', dle);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMultisig() {
|
// function openMultisig() {
|
||||||
router.push('/management/multisig');
|
// router.push('/management/multisig');
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Функции с передачей адреса DLE
|
// Функции с передачей адреса DLE
|
||||||
function openProposalsWithDle() {
|
function openProposalsWithDle() {
|
||||||
@@ -375,11 +392,11 @@ function openSettingsWithDle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMultisigWithDle() {
|
// function openMultisigWithDle() {
|
||||||
if (selectedDle.value) {
|
// if (selectedDle.value) {
|
||||||
router.push(`/management/multisig?address=${selectedDle.value.dleAddress}`);
|
// router.push(`/management/multisig?address=${selectedDle.value.dleAddress}`);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -123,10 +123,17 @@ async function removeToken(index) {
|
|||||||
const token = props.authTokens[index];
|
const token = props.authTokens[index];
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
if (!confirm(`Удалить токен ${token.name} (${token.address})?`)) return;
|
if (!confirm(`Удалить токен ${token.name} (${token.address})?`)) return;
|
||||||
|
|
||||||
|
console.log('[AuthTokensSettings] Удаление токена:', token);
|
||||||
|
console.log('[AuthTokensSettings] URL:', `/settings/auth-token/${token.address}/${token.network}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.delete(`/api/settings/auth-token/${token.address}/${token.network}`);
|
const response = await api.delete(`/settings/auth-token/${token.address}/${token.network}`);
|
||||||
|
console.log('[AuthTokensSettings] Успешное удаление:', response.data);
|
||||||
emit('update');
|
emit('update');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('[AuthTokensSettings] Ошибка при удалении токена:', e);
|
||||||
|
console.error('[AuthTokensSettings] Response:', e.response);
|
||||||
alert('Ошибка при удалении токена: ' + (e.response?.data?.error || e.message));
|
alert('Ошибка при удалении токена: ' + (e.response?.data?.error || e.message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,39 +238,8 @@ const proposalsChange = ref(8);
|
|||||||
const yieldRate = ref(8.7);
|
const yieldRate = ref(8.7);
|
||||||
const yieldChange = ref(1.2);
|
const yieldChange = ref(1.2);
|
||||||
|
|
||||||
// Топ участников (временные данные)
|
// Топ участников (загружаются из блокчейна)
|
||||||
const topParticipants = ref([
|
const topParticipants = ref([]);
|
||||||
{
|
|
||||||
rank: 1,
|
|
||||||
address: '0x1234567890123456789012345678901234567890',
|
|
||||||
balance: 2500,
|
|
||||||
percentage: 25.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 2,
|
|
||||||
address: '0x2345678901234567890123456789012345678901',
|
|
||||||
balance: 1800,
|
|
||||||
percentage: 18.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 3,
|
|
||||||
address: '0x3456789012345678901234567890123456789012',
|
|
||||||
balance: 1200,
|
|
||||||
percentage: 12.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 4,
|
|
||||||
address: '0x4567890123456789012345678901234567890123',
|
|
||||||
balance: 800,
|
|
||||||
percentage: 8.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rank: 5,
|
|
||||||
address: '0x5678901234567890123456789012345678901234',
|
|
||||||
balance: 600,
|
|
||||||
percentage: 6.0
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
const formatAddress = (address) => {
|
const formatAddress = (address) => {
|
||||||
|
|||||||
@@ -1,726 +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="dle-multisig-management">
|
|
||||||
<div class="multisig-header">
|
|
||||||
<h3>🔐 Управление мультиподписью</h3>
|
|
||||||
<button class="btn btn-primary" @click="showCreateForm = true">
|
|
||||||
<i class="fas fa-plus"></i> Создать операцию
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Форма создания мультиподписи -->
|
|
||||||
<div v-if="showCreateForm" class="create-multisig-form">
|
|
||||||
<div class="form-header">
|
|
||||||
<h4>🔐 Новая мультиподпись</h4>
|
|
||||||
<button class="close-btn" @click="showCreateForm = false">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-content">
|
|
||||||
<!-- Описание операции -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h5>📝 Описание операции</h5>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="operationDescription">Описание операции:</label>
|
|
||||||
<textarea
|
|
||||||
id="operationDescription"
|
|
||||||
v-model="newOperation.description"
|
|
||||||
class="form-control"
|
|
||||||
rows="3"
|
|
||||||
placeholder="Опишите, что нужно сделать..."
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="operationDuration">Длительность сбора подписей (дни):</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="operationDuration"
|
|
||||||
v-model.number="newOperation.duration"
|
|
||||||
class="form-control"
|
|
||||||
min="1"
|
|
||||||
max="30"
|
|
||||||
placeholder="7"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Тип операции -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h5>⚙️ Тип операции</h5>
|
|
||||||
|
|
||||||
<div class="operation-types">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="multisigOperationType">Выберите тип операции:</label>
|
|
||||||
<select id="multisigOperationType" v-model="newOperation.operationType" class="form-control">
|
|
||||||
<option value="">-- Выберите тип --</option>
|
|
||||||
<option value="transfer">Передача токенов</option>
|
|
||||||
<option value="mint">Минтинг токенов</option>
|
|
||||||
<option value="burn">Сжигание токенов</option>
|
|
||||||
<option value="addModule">Добавить модуль</option>
|
|
||||||
<option value="removeModule">Удалить модуль</option>
|
|
||||||
<option value="custom">Пользовательская операция</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Параметры для передачи токенов -->
|
|
||||||
<div v-if="newOperation.operationType === 'transfer'" class="operation-params">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="multisigTransferTo">Адрес получателя:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="multisigTransferTo"
|
|
||||||
v-model="newOperation.operationParams.to"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="0x..."
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="multisigTransferAmount">Количество токенов:</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="multisigTransferAmount"
|
|
||||||
v-model.number="newOperation.operationParams.amount"
|
|
||||||
class="form-control"
|
|
||||||
min="1"
|
|
||||||
placeholder="100"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Параметры для модулей -->
|
|
||||||
<div v-if="newOperation.operationType === 'addModule' || newOperation.operationType === 'removeModule'" class="operation-params">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="moduleId">ID модуля:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="moduleId"
|
|
||||||
v-model="newOperation.operationParams.moduleId"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="TreasuryModule"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div v-if="newOperation.operationType === 'addModule'" class="form-group">
|
|
||||||
<label for="moduleAddress">Адрес модуля:</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="moduleAddress"
|
|
||||||
v-model="newOperation.operationParams.moduleAddress"
|
|
||||||
class="form-control"
|
|
||||||
placeholder="0x..."
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Пользовательская операция -->
|
|
||||||
<div v-if="newOperation.operationType === 'custom'" class="operation-params">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="customMultisigOperation">Пользовательская операция (hex):</label>
|
|
||||||
<textarea
|
|
||||||
id="customMultisigOperation"
|
|
||||||
v-model="newOperation.operationParams.customData"
|
|
||||||
class="form-control"
|
|
||||||
rows="3"
|
|
||||||
placeholder="0x..."
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Предварительный просмотр -->
|
|
||||||
<div class="form-section">
|
|
||||||
<h5>👁️ Предварительный просмотр</h5>
|
|
||||||
<div class="preview-card">
|
|
||||||
<div class="preview-item">
|
|
||||||
<strong>Описание:</strong> {{ newOperation.description || 'Не указано' }}
|
|
||||||
</div>
|
|
||||||
<div class="preview-item">
|
|
||||||
<strong>Длительность:</strong> {{ newOperation.duration || 7 }} дней
|
|
||||||
</div>
|
|
||||||
<div class="preview-item">
|
|
||||||
<strong>Тип операции:</strong> {{ getOperationTypeName(newOperation.operationType) || 'Не выбран' }}
|
|
||||||
</div>
|
|
||||||
<div v-if="newOperation.operationType" class="preview-item">
|
|
||||||
<strong>Параметры:</strong> {{ getOperationParamsPreview() }}
|
|
||||||
</div>
|
|
||||||
<div class="preview-item">
|
|
||||||
<strong>Хеш операции:</strong> {{ getOperationHash() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Действия -->
|
|
||||||
<div class="form-actions">
|
|
||||||
<button
|
|
||||||
class="btn btn-success"
|
|
||||||
@click="createMultisigOperation"
|
|
||||||
:disabled="!isFormValid || isCreating"
|
|
||||||
>
|
|
||||||
<i class="fas fa-paper-plane"></i>
|
|
||||||
{{ isCreating ? 'Создание...' : 'Создать операцию' }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" @click="resetForm">
|
|
||||||
<i class="fas fa-undo"></i> Сбросить
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" @click="showCreateForm = false">
|
|
||||||
<i class="fas fa-times"></i> Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Список операций мультиподписи -->
|
|
||||||
<div class="multisig-list">
|
|
||||||
<div class="list-header">
|
|
||||||
<h4>📋 Список операций мультиподписи</h4>
|
|
||||||
<div class="list-filters">
|
|
||||||
<select v-model="statusFilter" class="form-control">
|
|
||||||
<option value="">Все статусы</option>
|
|
||||||
<option value="active">Активные</option>
|
|
||||||
<option value="pending">Ожидающие</option>
|
|
||||||
<option value="succeeded">Принятые</option>
|
|
||||||
<option value="defeated">Отклоненные</option>
|
|
||||||
<option value="executed">Выполненные</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="filteredOperations.length === 0" class="no-operations">
|
|
||||||
<p>Операций мультиподписи пока нет</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="operations-grid">
|
|
||||||
<div
|
|
||||||
v-for="operation in filteredOperations"
|
|
||||||
:key="operation.id"
|
|
||||||
class="operation-card"
|
|
||||||
:class="operation.status"
|
|
||||||
>
|
|
||||||
<div class="operation-header">
|
|
||||||
<h5>{{ operation.description }}</h5>
|
|
||||||
<span class="operation-status" :class="operation.status">
|
|
||||||
{{ getOperationStatusText(operation.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="operation-details">
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>ID:</strong> #{{ operation.id }}
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Создатель:</strong> {{ shortenAddress(operation.initiator) }}
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Хеш:</strong> {{ shortenAddress(operation.operationHash) }}
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Дедлайн:</strong> {{ formatDate(operation.deadline) }}
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Подписи:</strong>
|
|
||||||
<span class="signatures">
|
|
||||||
<span class="for">За: {{ operation.forSignatures }}</span>
|
|
||||||
<span class="against">Против: {{ operation.againstSignatures }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="operation-actions">
|
|
||||||
<button
|
|
||||||
v-if="canSign(operation)"
|
|
||||||
class="btn btn-sm btn-success"
|
|
||||||
@click="signOperation(operation.id, true)"
|
|
||||||
:disabled="hasSigned(operation.id, true)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-thumbs-up"></i> Подписать за
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canSign(operation)"
|
|
||||||
class="btn btn-sm btn-danger"
|
|
||||||
@click="signOperation(operation.id, false)"
|
|
||||||
:disabled="hasSigned(operation.id, false)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-thumbs-down"></i> Подписать против
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canExecute(operation)"
|
|
||||||
class="btn btn-sm btn-primary"
|
|
||||||
@click="executeOperation(operation.id)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-play"></i> Исполнить
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-info"
|
|
||||||
@click="viewOperationDetails(operation.id)"
|
|
||||||
>
|
|
||||||
<i class="fas fa-eye"></i> Детали
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed, onMounted, defineProps, defineEmits } from 'vue';
|
|
||||||
import { useAuthContext } from '@/composables/useAuth';
|
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dleAddress: { type: String, required: false, default: null },
|
|
||||||
dleContract: { type: Object, required: false, default: null },
|
|
||||||
isAuthenticated: Boolean,
|
|
||||||
identities: Array,
|
|
||||||
tokenBalances: Object,
|
|
||||||
isLoadingTokens: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
|
||||||
|
|
||||||
const { address } = useAuthContext();
|
|
||||||
|
|
||||||
// Состояние формы
|
|
||||||
const showCreateForm = ref(false);
|
|
||||||
const isCreating = ref(false);
|
|
||||||
const statusFilter = ref('');
|
|
||||||
|
|
||||||
// Новая операция
|
|
||||||
const newOperation = ref({
|
|
||||||
description: '',
|
|
||||||
duration: 7,
|
|
||||||
operationType: '',
|
|
||||||
operationParams: {
|
|
||||||
to: '',
|
|
||||||
from: '',
|
|
||||||
amount: 0,
|
|
||||||
moduleId: '',
|
|
||||||
moduleAddress: '',
|
|
||||||
customData: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Операции мультиподписи
|
|
||||||
const operations = ref([]);
|
|
||||||
|
|
||||||
// Вычисляемые свойства
|
|
||||||
const isFormValid = computed(() => {
|
|
||||||
return (
|
|
||||||
newOperation.value.description &&
|
|
||||||
newOperation.value.duration > 0 &&
|
|
||||||
newOperation.value.operationType &&
|
|
||||||
validateOperationParams()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredOperations = computed(() => {
|
|
||||||
if (!statusFilter.value) return operations.value;
|
|
||||||
return operations.value.filter(o => o.status === statusFilter.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функции
|
|
||||||
function validateOperationParams() {
|
|
||||||
const params = newOperation.value.operationParams;
|
|
||||||
|
|
||||||
switch (newOperation.value.operationType) {
|
|
||||||
case 'transfer':
|
|
||||||
case 'mint':
|
|
||||||
return params.to && params.amount > 0;
|
|
||||||
case 'burn':
|
|
||||||
return params.from && params.amount > 0;
|
|
||||||
case 'addModule':
|
|
||||||
return params.moduleId && params.moduleAddress;
|
|
||||||
case 'removeModule':
|
|
||||||
return params.moduleId;
|
|
||||||
case 'custom':
|
|
||||||
return params.customData && params.customData.startsWith('0x');
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOperationTypeName(type) {
|
|
||||||
const types = {
|
|
||||||
'transfer': 'Передача токенов',
|
|
||||||
'mint': 'Минтинг токенов',
|
|
||||||
'burn': 'Сжигание токенов',
|
|
||||||
'addModule': 'Добавить модуль',
|
|
||||||
'removeModule': 'Удалить модуль',
|
|
||||||
'custom': 'Пользовательская операция'
|
|
||||||
};
|
|
||||||
return types[type] || 'Неизвестный тип';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOperationParamsPreview() {
|
|
||||||
const params = newOperation.value.operationParams;
|
|
||||||
|
|
||||||
switch (newOperation.value.operationType) {
|
|
||||||
case 'transfer':
|
|
||||||
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
|
|
||||||
case 'mint':
|
|
||||||
return `Кому: ${shortenAddress(params.to)}, Количество: ${params.amount}`;
|
|
||||||
case 'burn':
|
|
||||||
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
|
|
||||||
case 'addModule':
|
|
||||||
return `ID: ${params.moduleId}, Адрес: ${shortenAddress(params.moduleAddress)}`;
|
|
||||||
case 'removeModule':
|
|
||||||
return `ID: ${params.moduleId}`;
|
|
||||||
case 'custom':
|
|
||||||
return `Данные: ${params.customData.substring(0, 20)}...`;
|
|
||||||
default:
|
|
||||||
return 'Не указаны';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOperationHash() {
|
|
||||||
// Генерируем хеш операции на основе параметров
|
|
||||||
const params = newOperation.value.operationParams;
|
|
||||||
const operationData = JSON.stringify({
|
|
||||||
type: newOperation.value.operationType,
|
|
||||||
params: params
|
|
||||||
});
|
|
||||||
|
|
||||||
// Простой хеш для демонстрации
|
|
||||||
return '0x' + btoa(operationData).substring(0, 64);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shortenAddress(address) {
|
|
||||||
if (!address) return '';
|
|
||||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(timestamp) {
|
|
||||||
if (!timestamp) return 'N/A';
|
|
||||||
return new Date(timestamp * 1000).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOperationStatusText(status) {
|
|
||||||
const statusMap = {
|
|
||||||
'pending': 'Ожидает',
|
|
||||||
'active': 'Активно',
|
|
||||||
'succeeded': 'Принято',
|
|
||||||
'defeated': 'Отклонено',
|
|
||||||
'executed': 'Выполнено'
|
|
||||||
};
|
|
||||||
return statusMap[status] || status;
|
|
||||||
}
|
|
||||||
|
|
||||||
function canSign(operation) {
|
|
||||||
return operation.status === 'active' && !hasSigned(operation.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function canExecute(operation) {
|
|
||||||
return operation.status === 'succeeded' && !operation.executed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasSigned(operationId, support = null) {
|
|
||||||
// Здесь должна быть проверка подписи пользователя
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создание операции мультиподписи
|
|
||||||
async function createMultisigOperation() {
|
|
||||||
if (!isFormValid.value) {
|
|
||||||
alert('Пожалуйста, заполните все обязательные поля');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isCreating.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Генерируем хеш операции
|
|
||||||
const operationHash = getOperationHash();
|
|
||||||
|
|
||||||
// Вызов смарт-контракта
|
|
||||||
const tx = await props.dleContract.createMultiSigOperation(
|
|
||||||
operationHash,
|
|
||||||
newOperation.value.duration * 24 * 60 * 60 // конвертируем в секунды
|
|
||||||
);
|
|
||||||
|
|
||||||
await tx.wait();
|
|
||||||
|
|
||||||
// Обновляем список операций
|
|
||||||
await loadOperations();
|
|
||||||
|
|
||||||
// Сбрасываем форму
|
|
||||||
resetForm();
|
|
||||||
showCreateForm.value = false;
|
|
||||||
|
|
||||||
alert('✅ Операция мультиподписи успешно создана!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка при создании операции мультиподписи:', error);
|
|
||||||
alert('❌ Ошибка при создании операции: ' + error.message);
|
|
||||||
} finally {
|
|
||||||
isCreating.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подписание операции
|
|
||||||
async function signOperation(operationId, support) {
|
|
||||||
try {
|
|
||||||
const tx = await props.dleContract.signMultiSigOperation(operationId, support);
|
|
||||||
await tx.wait();
|
|
||||||
|
|
||||||
await loadOperations();
|
|
||||||
alert('✅ Ваша подпись учтена!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка при подписании операции:', error);
|
|
||||||
alert('❌ Ошибка при подписании: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Исполнение операции
|
|
||||||
async function executeOperation(operationId) {
|
|
||||||
try {
|
|
||||||
const tx = await props.dleContract.executeMultiSigOperation(operationId);
|
|
||||||
await tx.wait();
|
|
||||||
|
|
||||||
await loadOperations();
|
|
||||||
alert('✅ Операция успешно исполнена!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка при исполнении операции:', error);
|
|
||||||
alert('❌ Ошибка при исполнении операции: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загрузка операций
|
|
||||||
async function loadOperations() {
|
|
||||||
try {
|
|
||||||
// Здесь должен быть вызов API или смарт-контракта для загрузки операций
|
|
||||||
// Пока используем заглушку
|
|
||||||
operations.value = [];
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка при загрузке операций:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
newOperation.value = {
|
|
||||||
description: '',
|
|
||||||
duration: 7,
|
|
||||||
operationType: '',
|
|
||||||
operationParams: {
|
|
||||||
to: '',
|
|
||||||
from: '',
|
|
||||||
amount: 0,
|
|
||||||
moduleId: '',
|
|
||||||
moduleAddress: '',
|
|
||||||
customData: ''
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function viewOperationDetails(operationId) {
|
|
||||||
// Открыть модальное окно с деталями операции
|
|
||||||
// console.log('Просмотр деталей операции:', operationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loadOperations();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dle-multisig-management {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multisig-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-multisig-form {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section h5 {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-types {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-params {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-item {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multisig-list {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operations-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-card {
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-card.active {
|
|
||||||
border-color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-card.succeeded {
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-card.defeated {
|
|
||||||
border-color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-header h5 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-status {
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-status.active {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-status.succeeded {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-status.defeated {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-details {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-item {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signatures {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signatures .for {
|
|
||||||
color: #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signatures .against {
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operation-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-operations {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -293,85 +293,8 @@ const filters = ref({
|
|||||||
status: ''
|
status: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// История операций (временные данные)
|
// История операций (загружается из блокчейна)
|
||||||
const history = ref([
|
const history = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: 'proposal',
|
|
||||||
title: 'Создание предложения',
|
|
||||||
description: 'Создано предложение #15: Перевод 100 токенов партнеру',
|
|
||||||
timestamp: Date.now() - 3600000,
|
|
||||||
status: 'success',
|
|
||||||
transactionHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
||||||
blockNumber: 18456789,
|
|
||||||
data: {
|
|
||||||
'ID предложения': 15,
|
|
||||||
'Инициатор': '0x1234...5678',
|
|
||||||
'Количество токенов': '100 MDLE',
|
|
||||||
'Получатель': '0xabcd...efgh'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 'vote',
|
|
||||||
title: 'Голосование',
|
|
||||||
description: 'Подписано предложение #15',
|
|
||||||
timestamp: Date.now() - 7200000,
|
|
||||||
status: 'success',
|
|
||||||
transactionHash: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890',
|
|
||||||
blockNumber: 18456788,
|
|
||||||
data: {
|
|
||||||
'ID предложения': 15,
|
|
||||||
'Голосующий': '0x5678...9012',
|
|
||||||
'Вес голоса': '500 токенов'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
type: 'transfer',
|
|
||||||
title: 'Трансфер токенов',
|
|
||||||
description: 'Перевод 50 токенов между участниками',
|
|
||||||
timestamp: Date.now() - 10800000,
|
|
||||||
status: 'success',
|
|
||||||
transactionHash: '0x567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234',
|
|
||||||
blockNumber: 18456787,
|
|
||||||
data: {
|
|
||||||
'От': '0x9012...3456',
|
|
||||||
'Кому': '0x3456...7890',
|
|
||||||
'Количество': '50 MDLE'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
type: 'treasury',
|
|
||||||
title: 'Операция с казной',
|
|
||||||
description: 'Пополнение казны на 1000 USDC',
|
|
||||||
timestamp: Date.now() - 14400000,
|
|
||||||
status: 'pending',
|
|
||||||
transactionHash: '0x901234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd',
|
|
||||||
blockNumber: 18456786,
|
|
||||||
data: {
|
|
||||||
'Тип операции': 'Депозит',
|
|
||||||
'Актив': 'USDC',
|
|
||||||
'Количество': '1000'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 5,
|
|
||||||
type: 'module',
|
|
||||||
title: 'Установка модуля',
|
|
||||||
description: 'Установлен модуль "Казначейство"',
|
|
||||||
timestamp: Date.now() - 18000000,
|
|
||||||
status: 'success',
|
|
||||||
transactionHash: '0x345678901234567890abcdef1234567890abcdef1234567890abcdef123456789',
|
|
||||||
blockNumber: 18456785,
|
|
||||||
data: {
|
|
||||||
'Название модуля': 'Казначейство',
|
|
||||||
'Версия': '1.0.0',
|
|
||||||
'Адрес контракта': '0xabcd...efgh'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Вычисляемые свойства
|
// Вычисляемые свойства
|
||||||
const filteredHistory = computed(() => {
|
const filteredHistory = computed(() => {
|
||||||
|
|||||||
@@ -1,766 +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="modules-container">
|
|
||||||
<!-- Заголовок -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>Модули DLE</h1>
|
|
||||||
<p>Установка, настройка и управление модулями</p>
|
|
||||||
</div>
|
|
||||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Доступные модули -->
|
|
||||||
<div class="available-modules-section">
|
|
||||||
<h2>Доступные модули</h2>
|
|
||||||
<div class="modules-grid">
|
|
||||||
<div
|
|
||||||
v-for="module in availableModules"
|
|
||||||
:key="module.id"
|
|
||||||
class="module-card"
|
|
||||||
:class="{ 'module-installed': module.installed }"
|
|
||||||
>
|
|
||||||
<div class="module-header">
|
|
||||||
<h3>{{ module.name }}</h3>
|
|
||||||
<span class="module-version">v{{ module.version }}</span>
|
|
||||||
</div>
|
|
||||||
<p class="module-description">{{ module.description }}</p>
|
|
||||||
<div class="module-features">
|
|
||||||
<span
|
|
||||||
v-for="feature in module.features"
|
|
||||||
:key="feature"
|
|
||||||
class="feature-tag"
|
|
||||||
>
|
|
||||||
{{ feature }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="module-actions">
|
|
||||||
<button
|
|
||||||
v-if="!module.installed"
|
|
||||||
@click="installModule(module.id)"
|
|
||||||
class="btn-primary"
|
|
||||||
:disabled="isInstalling"
|
|
||||||
>
|
|
||||||
{{ isInstalling ? 'Установка...' : 'Установить' }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
@click="openModuleInterface(module)"
|
|
||||||
class="btn-secondary"
|
|
||||||
>
|
|
||||||
Управление
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Установленные модули -->
|
|
||||||
<div class="installed-modules-section">
|
|
||||||
<h2>Установленные модули</h2>
|
|
||||||
<div v-if="installedModules.length === 0" class="empty-state">
|
|
||||||
<p>Нет установленных модулей</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="installed-modules-list">
|
|
||||||
<div
|
|
||||||
v-for="module in installedModules"
|
|
||||||
:key="module.address"
|
|
||||||
class="installed-module-card"
|
|
||||||
>
|
|
||||||
<div class="module-info">
|
|
||||||
<div class="module-header">
|
|
||||||
<h3>{{ module.name }}</h3>
|
|
||||||
<span class="module-status" :class="module.status">
|
|
||||||
{{ getStatusText(module.status) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="module-description">{{ module.description }}</p>
|
|
||||||
<p class="module-address">Адрес: {{ formatAddress(module.address) }}</p>
|
|
||||||
<p class="module-version">Версия: {{ module.version }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="module-actions">
|
|
||||||
<button @click="openModuleInterface(module)" class="btn-secondary">
|
|
||||||
Управление
|
|
||||||
</button>
|
|
||||||
<button @click="configureModule(module)" class="btn-secondary">
|
|
||||||
Настройки
|
|
||||||
</button>
|
|
||||||
<button @click="uninstallModule(module.address)" class="btn-danger">
|
|
||||||
Удалить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Модальное окно настройки модуля -->
|
|
||||||
<div v-if="showConfigModal" class="modal-overlay" @click="showConfigModal = false">
|
|
||||||
<div class="modal-content" @click.stop>
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Настройки модуля {{ selectedModule?.name }}</h3>
|
|
||||||
<button @click="showConfigModal = false" class="close-btn">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form @submit.prevent="saveModuleConfig" class="config-form">
|
|
||||||
<div
|
|
||||||
v-for="setting in selectedModule?.configSettings"
|
|
||||||
:key="setting.key"
|
|
||||||
class="form-group"
|
|
||||||
>
|
|
||||||
<label :for="setting.key">{{ setting.label }}:</label>
|
|
||||||
<input
|
|
||||||
v-if="setting.type === 'text'"
|
|
||||||
:id="setting.key"
|
|
||||||
v-model="moduleConfig[setting.key]"
|
|
||||||
type="text"
|
|
||||||
:placeholder="setting.placeholder"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-else-if="setting.type === 'number'"
|
|
||||||
:id="setting.key"
|
|
||||||
v-model="moduleConfig[setting.key]"
|
|
||||||
type="number"
|
|
||||||
:min="setting.min"
|
|
||||||
:max="setting.max"
|
|
||||||
:placeholder="setting.placeholder"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
v-else-if="setting.type === 'select'"
|
|
||||||
:id="setting.key"
|
|
||||||
v-model="moduleConfig[setting.key]"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Выберите значение</option>
|
|
||||||
<option
|
|
||||||
v-for="option in setting.options"
|
|
||||||
:key="option.value"
|
|
||||||
:value="option.value"
|
|
||||||
>
|
|
||||||
{{ option.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span class="setting-hint">{{ setting.hint }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="button" @click="showConfigModal = false" class="btn-secondary">
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isSavingConfig">
|
|
||||||
{{ isSavingConfig ? 'Сохранение...' : 'Сохранить' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, defineProps, defineEmits } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
|
||||||
|
|
||||||
// Определяем props
|
|
||||||
const props = defineProps({
|
|
||||||
isAuthenticated: Boolean,
|
|
||||||
identities: Array,
|
|
||||||
tokenBalances: Object,
|
|
||||||
isLoadingTokens: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем emits
|
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Состояние
|
|
||||||
const isInstalling = ref(false);
|
|
||||||
const isSavingConfig = ref(false);
|
|
||||||
const showConfigModal = ref(false);
|
|
||||||
const selectedModule = ref(null);
|
|
||||||
const moduleConfig = ref({});
|
|
||||||
|
|
||||||
// Доступные модули (временные данные)
|
|
||||||
const availableModules = ref([
|
|
||||||
{
|
|
||||||
id: 'treasury',
|
|
||||||
name: 'Казначейство',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'Управление средствами и активами DLE',
|
|
||||||
features: ['Мультивалютность', 'Автоматизация', 'Отчетность'],
|
|
||||||
installed: true,
|
|
||||||
configSettings: [
|
|
||||||
{
|
|
||||||
key: 'maxWithdrawal',
|
|
||||||
label: 'Максимальная сумма вывода',
|
|
||||||
type: 'number',
|
|
||||||
min: 0,
|
|
||||||
max: 1000000,
|
|
||||||
placeholder: '10000',
|
|
||||||
hint: 'Максимальная сумма для однократного вывода средств'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'approvalRequired',
|
|
||||||
label: 'Требуется одобрение',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ value: 'true', label: 'Да' },
|
|
||||||
{ value: 'false', label: 'Нет' }
|
|
||||||
],
|
|
||||||
hint: 'Требуется ли одобрение для операций с казной'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'governance',
|
|
||||||
name: 'Расширенное управление',
|
|
||||||
version: '2.1.0',
|
|
||||||
description: 'Дополнительные функции голосования и управления',
|
|
||||||
features: ['Делегирование', 'Взвешенное голосование', 'Автоматизация'],
|
|
||||||
installed: false,
|
|
||||||
configSettings: [
|
|
||||||
{
|
|
||||||
key: 'delegationEnabled',
|
|
||||||
label: 'Включить делегирование',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ value: 'true', label: 'Да' },
|
|
||||||
{ value: 'false', label: 'Нет' }
|
|
||||||
],
|
|
||||||
hint: 'Разрешить делегирование голосов'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'compliance',
|
|
||||||
name: 'Соответствие требованиям',
|
|
||||||
version: '1.2.0',
|
|
||||||
description: 'Модуль для обеспечения соответствия нормативным требованиям',
|
|
||||||
features: ['KYC/AML', 'Отчетность', 'Аудит'],
|
|
||||||
installed: false,
|
|
||||||
configSettings: [
|
|
||||||
{
|
|
||||||
key: 'kycRequired',
|
|
||||||
label: 'Требуется KYC',
|
|
||||||
type: 'select',
|
|
||||||
options: [
|
|
||||||
{ value: 'true', label: 'Да' },
|
|
||||||
{ value: 'false', label: 'Нет' }
|
|
||||||
],
|
|
||||||
hint: 'Требуется ли прохождение KYC для участия'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Установленные модули (временные данные)
|
|
||||||
const installedModules = ref([
|
|
||||||
{
|
|
||||||
name: 'Казначейство',
|
|
||||||
description: 'Управление средствами и активами DLE',
|
|
||||||
address: '0x1234567890123456789012345678901234567890',
|
|
||||||
version: '1.0.0',
|
|
||||||
status: 'active',
|
|
||||||
configSettings: availableModules.value[0].configSettings
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Методы
|
|
||||||
const installModule = async (moduleId) => {
|
|
||||||
if (isInstalling.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isInstalling.value = true;
|
|
||||||
|
|
||||||
// Здесь будет логика установки модуля
|
|
||||||
console.log('Установка модуля:', moduleId);
|
|
||||||
|
|
||||||
// Временная логика
|
|
||||||
const module = availableModules.value.find(m => m.id === moduleId);
|
|
||||||
if (module) {
|
|
||||||
module.installed = true;
|
|
||||||
|
|
||||||
// Добавляем в список установленных
|
|
||||||
installedModules.value.push({
|
|
||||||
name: module.name,
|
|
||||||
description: module.description,
|
|
||||||
address: '0x' + Math.random().toString(16).substr(2, 40),
|
|
||||||
version: module.version,
|
|
||||||
status: 'active',
|
|
||||||
configSettings: module.configSettings
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
alert('Модуль успешно установлен!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка установки модуля:', error);
|
|
||||||
alert('Ошибка при установке модуля');
|
|
||||||
} finally {
|
|
||||||
isInstalling.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const uninstallModule = async (moduleAddress) => {
|
|
||||||
if (!confirm('Вы уверены, что хотите удалить этот модуль?')) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Здесь будет логика удаления модуля
|
|
||||||
console.log('Удаление модуля:', moduleAddress);
|
|
||||||
|
|
||||||
// Временная логика
|
|
||||||
installedModules.value = installedModules.value.filter(m => m.address !== moduleAddress);
|
|
||||||
|
|
||||||
// Обновляем статус в доступных модулях
|
|
||||||
const module = availableModules.value.find(m => m.name === 'Казначейство');
|
|
||||||
if (module) {
|
|
||||||
module.installed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
alert('Модуль успешно удален!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка удаления модуля:', error);
|
|
||||||
alert('Ошибка при удалении модуля');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const configureModule = (module) => {
|
|
||||||
selectedModule.value = module;
|
|
||||||
moduleConfig.value = {};
|
|
||||||
showConfigModal.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveModuleConfig = async () => {
|
|
||||||
if (isSavingConfig.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isSavingConfig.value = true;
|
|
||||||
|
|
||||||
// Здесь будет логика сохранения конфигурации
|
|
||||||
console.log('Сохранение конфигурации:', moduleConfig.value);
|
|
||||||
|
|
||||||
alert('Конфигурация успешно сохранена!');
|
|
||||||
showConfigModal.value = false;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка сохранения конфигурации:', error);
|
|
||||||
alert('Ошибка при сохранении конфигурации');
|
|
||||||
} finally {
|
|
||||||
isSavingConfig.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModuleInterface = (module) => {
|
|
||||||
// Здесь будет логика открытия интерфейса модуля
|
|
||||||
console.log('Открытие интерфейса модуля:', module);
|
|
||||||
alert(`Открытие интерфейса модуля ${module.name}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status) => {
|
|
||||||
const statusMap = {
|
|
||||||
'active': 'Активен',
|
|
||||||
'inactive': 'Неактивен',
|
|
||||||
'error': 'Ошибка',
|
|
||||||
'updating': 'Обновляется'
|
|
||||||
};
|
|
||||||
return statusMap[status] || status;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAddress = (address) => {
|
|
||||||
if (!address) return '';
|
|
||||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.modules-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Секции */
|
|
||||||
.available-modules-section,
|
|
||||||
.installed-modules-section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.available-modules-section h2,
|
|
||||||
.installed-modules-section h2 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка модулей */
|
|
||||||
.modules-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-card {
|
|
||||||
background: white;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-card.module-installed {
|
|
||||||
border-left: 4px solid #28a745;
|
|
||||||
background: #f8fff9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-version {
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-description {
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-features {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-tag {
|
|
||||||
background: #e9ecef;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Установленные модули */
|
|
||||||
.installed-modules-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.installed-module-card {
|
|
||||||
background: white;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.installed-module-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-info {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-status {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-status.active {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-status.inactive {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-status.error {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-status.updating {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-address {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-version {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Модальное окно */
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
width: 90%;
|
|
||||||
max-width: 600px;
|
|
||||||
max-height: 90vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Форма конфигурации */
|
|
||||||
.config-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input,
|
|
||||||
.form-group select {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-hint {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-grey-dark);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки */
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--color-primary);
|
|
||||||
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-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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: #dc3545;
|
|
||||||
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-danger:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Состояния */
|
|
||||||
.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) {
|
|
||||||
.modules-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,611 +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="proposals-container">
|
|
||||||
<!-- Заголовок -->
|
|
||||||
<div class="page-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<h1>Предложения</h1>
|
|
||||||
<p>Создание, подписание и выполнение предложений</p>
|
|
||||||
</div>
|
|
||||||
<button class="close-btn" @click="router.push('/management')">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Создание нового предложения -->
|
|
||||||
<div class="create-proposal-section">
|
|
||||||
<h2>Создать новое предложение</h2>
|
|
||||||
<form @submit.prevent="createProposal" class="proposal-form">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="operationType">Тип операции:</label>
|
|
||||||
<select id="operationType" v-model="newProposal.operationType" required>
|
|
||||||
<option value="">Выберите тип операции</option>
|
|
||||||
<option value="token_transfer">Перевод токенов</option>
|
|
||||||
<option value="treasury_operation">Казначейская операция</option>
|
|
||||||
<option value="module_install">Установка модуля</option>
|
|
||||||
<option value="parameter_change">Изменение параметров</option>
|
|
||||||
<option value="emergency_action">Экстренные действия</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="timelockDelay">Задержка таймлока (часы):</label>
|
|
||||||
<input
|
|
||||||
id="timelockDelay"
|
|
||||||
type="number"
|
|
||||||
v-model="newProposal.timelockDelay"
|
|
||||||
min="1"
|
|
||||||
max="168"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="description">Описание операции:</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
v-model="newProposal.description"
|
|
||||||
placeholder="Опишите детали операции..."
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Целевые сети:</label>
|
|
||||||
<div class="networks-grid">
|
|
||||||
<label v-for="network in availableNetworks" :key="network.id" class="network-checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:value="network.id"
|
|
||||||
v-model="newProposal.targetChains"
|
|
||||||
>
|
|
||||||
{{ network.name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary" :disabled="isCreatingProposal">
|
|
||||||
{{ isCreatingProposal ? 'Создание...' : 'Создать предложение' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Активные предложения -->
|
|
||||||
<div class="proposals-section">
|
|
||||||
<h2>Активные предложения</h2>
|
|
||||||
<div v-if="proposals.length === 0" class="empty-state">
|
|
||||||
<p>Нет активных предложений</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="proposals-list">
|
|
||||||
<div
|
|
||||||
v-for="proposal in proposals"
|
|
||||||
:key="proposal.id"
|
|
||||||
class="proposal-card"
|
|
||||||
:class="{ 'proposal-executed': proposal.executed }"
|
|
||||||
>
|
|
||||||
<div class="proposal-header">
|
|
||||||
<h3>Предложение #{{ proposal.id }}</h3>
|
|
||||||
<span class="proposal-status" :class="getStatusClass(proposal)">
|
|
||||||
{{ getStatusText(proposal) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="proposal-details">
|
|
||||||
<p><strong>Описание:</strong> {{ proposal.description }}</p>
|
|
||||||
<p><strong>Инициатор:</strong> {{ formatAddress(proposal.initiator) }}</p>
|
|
||||||
<p><strong>Таймлок:</strong> {{ formatTimestamp(proposal.timelock) }}</p>
|
|
||||||
<p><strong>Подписи:</strong> {{ proposal.signaturesCount }} / {{ proposal.quorumRequired }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="proposal-actions">
|
|
||||||
<button
|
|
||||||
v-if="!proposal.hasSigned && !proposal.executed"
|
|
||||||
@click="signProposal(proposal.id)"
|
|
||||||
class="btn-secondary"
|
|
||||||
:disabled="isSigning"
|
|
||||||
>
|
|
||||||
Подписать
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="canExecuteProposal(proposal)"
|
|
||||||
@click="executeProposal(proposal.id)"
|
|
||||||
class="btn-success"
|
|
||||||
:disabled="isExecuting"
|
|
||||||
>
|
|
||||||
Выполнить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, defineProps, defineEmits } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
|
||||||
|
|
||||||
// Определяем props
|
|
||||||
const props = defineProps({
|
|
||||||
isAuthenticated: Boolean,
|
|
||||||
identities: Array,
|
|
||||||
tokenBalances: Object,
|
|
||||||
isLoadingTokens: Boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем emits
|
|
||||||
const emit = defineEmits(['auth-action-completed']);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// Состояние
|
|
||||||
const isCreatingProposal = ref(false);
|
|
||||||
const isSigning = ref(false);
|
|
||||||
const isExecuting = ref(false);
|
|
||||||
|
|
||||||
const newProposal = ref({
|
|
||||||
operationType: '',
|
|
||||||
description: '',
|
|
||||||
targetChains: [],
|
|
||||||
timelockDelay: 24
|
|
||||||
});
|
|
||||||
|
|
||||||
// Доступные сети
|
|
||||||
const availableNetworks = ref([
|
|
||||||
{ id: 1, name: 'Ethereum Mainnet' },
|
|
||||||
{ id: 137, name: 'Polygon' },
|
|
||||||
{ id: 56, name: 'BSC' },
|
|
||||||
{ id: 42161, name: 'Arbitrum' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Предложения (временные данные)
|
|
||||||
const proposals = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
description: 'Перевод 100 токенов партнеру',
|
|
||||||
initiator: '0x1234567890123456789012345678901234567890',
|
|
||||||
timelock: Math.floor(Date.now() / 1000) + 3600,
|
|
||||||
signaturesCount: 5000,
|
|
||||||
quorumRequired: 5100,
|
|
||||||
executed: false,
|
|
||||||
hasSigned: false
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Методы
|
|
||||||
const createProposal = async () => {
|
|
||||||
if (isCreatingProposal.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isCreatingProposal.value = true;
|
|
||||||
|
|
||||||
// Здесь будет создание предложения в смарт-контракте
|
|
||||||
// console.log('Создание предложения:', newProposal.value);
|
|
||||||
|
|
||||||
// Временная логика
|
|
||||||
const proposal = {
|
|
||||||
id: proposals.value.length + 1,
|
|
||||||
description: newProposal.value.description,
|
|
||||||
initiator: '0x' + Math.random().toString(16).substr(2, 40),
|
|
||||||
timelock: Math.floor(Date.now() / 1000) + (newProposal.value.timelockDelay * 3600),
|
|
||||||
signaturesCount: 0,
|
|
||||||
quorumRequired: 5100,
|
|
||||||
executed: false,
|
|
||||||
hasSigned: false
|
|
||||||
};
|
|
||||||
|
|
||||||
proposals.value.push(proposal);
|
|
||||||
|
|
||||||
// Сброс формы
|
|
||||||
newProposal.value = {
|
|
||||||
operationType: '',
|
|
||||||
description: '',
|
|
||||||
targetChains: [],
|
|
||||||
timelockDelay: 24
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка создания предложения:', error);
|
|
||||||
} finally {
|
|
||||||
isCreatingProposal.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const signProposal = async (proposalId) => {
|
|
||||||
if (isSigning.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isSigning.value = true;
|
|
||||||
|
|
||||||
// Здесь будет подписание предложения в смарт-контракте
|
|
||||||
// console.log('Подписание предложения:', proposalId);
|
|
||||||
|
|
||||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
|
||||||
if (proposal) {
|
|
||||||
proposal.signaturesCount += 1000;
|
|
||||||
proposal.hasSigned = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка подписания предложения:', error);
|
|
||||||
} finally {
|
|
||||||
isSigning.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const executeProposal = async (proposalId) => {
|
|
||||||
if (isExecuting.value) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
isExecuting.value = true;
|
|
||||||
|
|
||||||
// Здесь будет выполнение предложения в смарт-контракте
|
|
||||||
// console.log('Выполнение предложения:', proposalId);
|
|
||||||
|
|
||||||
const proposal = proposals.value.find(p => p.id === proposalId);
|
|
||||||
if (proposal) {
|
|
||||||
proposal.executed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Ошибка выполнения предложения:', error);
|
|
||||||
} finally {
|
|
||||||
isExecuting.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canExecuteProposal = (proposal) => {
|
|
||||||
return !proposal.executed &&
|
|
||||||
proposal.signaturesCount >= proposal.quorumRequired &&
|
|
||||||
Date.now() >= proposal.timelock * 1000;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAddress = (address) => {
|
|
||||||
if (!address) return '';
|
|
||||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp) => {
|
|
||||||
return new Date(timestamp * 1000).toLocaleString('ru-RU');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusClass = (proposal) => {
|
|
||||||
if (proposal.executed) return 'status-executed';
|
|
||||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'status-ready';
|
|
||||||
return 'status-pending';
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (proposal) => {
|
|
||||||
if (proposal.executed) return 'Выполнено';
|
|
||||||
if (proposal.signaturesCount >= proposal.quorumRequired) return 'Готово к выполнению';
|
|
||||||
return 'Ожидает подписей';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.proposals-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Секции */
|
|
||||||
.create-proposal-section,
|
|
||||||
.proposals-section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-proposal-section h2,
|
|
||||||
.proposals-section h2 {
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Форма */
|
|
||||||
.proposal-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 {
|
|
||||||
min-height: 100px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.networks-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-checkbox:hover {
|
|
||||||
background: #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-checkbox input[type="checkbox"] {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Предложения */
|
|
||||||
.proposals-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-card {
|
|
||||||
background: white;
|
|
||||||
padding: 25px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid #e9ecef;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-card.proposal-executed {
|
|
||||||
opacity: 0.7;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-size: 1.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-status {
|
|
||||||
padding: 6px 16px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-pending {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ready {
|
|
||||||
background: #d1ecf1;
|
|
||||||
color: #0c5460;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-executed {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-details {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-details p {
|
|
||||||
margin: 8px 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки */
|
|
||||||
.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:not(:disabled) {
|
|
||||||
background: var(--color-secondary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: #28a745;
|
|
||||||
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-success:hover:not(:disabled) {
|
|
||||||
background: #218838;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success: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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.proposal-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.networks-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -216,27 +216,8 @@ const newSettings = ref({
|
|||||||
reason: ''
|
reason: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// История изменений (временные данные)
|
// История изменений (загружается из блокчейна)
|
||||||
const settingsHistory = ref([
|
const settingsHistory = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
timestamp: Date.now() - 86400000, // 1 день назад
|
|
||||||
reason: 'Оптимизация параметров голосования для повышения эффективности',
|
|
||||||
quorumChange: { from: 60, to: 51 },
|
|
||||||
votingDelayChange: { from: 2, to: 1 },
|
|
||||||
author: '0x1234567890123456789012345678901234567890'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
timestamp: Date.now() - 604800000, // 1 неделя назад
|
|
||||||
reason: 'Первоначальная настройка параметров DLE',
|
|
||||||
quorumChange: { from: 0, to: 60 },
|
|
||||||
votingDelayChange: { from: 0, to: 2 },
|
|
||||||
votingPeriodChange: { from: 0, to: 45818 },
|
|
||||||
proposalThresholdChange: { from: 0, to: 100 },
|
|
||||||
author: '0x2345678901234567890123456789012345678901'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
const updateSettings = async () => {
|
const updateSettings = async () => {
|
||||||
|
|||||||
@@ -351,14 +351,8 @@ const networkSettings = ref({
|
|||||||
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
|
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Доступные сети
|
// Доступные сети (загружаются из конфигурации)
|
||||||
const availableNetworks = ref([
|
const availableNetworks = ref([]);
|
||||||
{ id: 1, name: 'Ethereum Mainnet', chainId: 1 },
|
|
||||||
{ id: 137, name: 'Polygon', chainId: 137 },
|
|
||||||
{ id: 56, name: 'BSC', chainId: 56 },
|
|
||||||
{ id: 42161, name: 'Arbitrum', chainId: 42161 },
|
|
||||||
{ id: 10, name: 'Optimism', chainId: 10 }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
const saveMainSettings = async () => {
|
const saveMainSettings = async () => {
|
||||||
|
|||||||
@@ -228,12 +228,12 @@ const isLoadingDle = ref(false);
|
|||||||
const isTransferring = ref(false);
|
const isTransferring = ref(false);
|
||||||
const isDistributing = ref(false);
|
const isDistributing = ref(false);
|
||||||
|
|
||||||
// Данные токенов (реактивные)
|
// Данные токенов (загружаются из блокчейна)
|
||||||
const tokenSymbol = computed(() => selectedDle.value?.symbol || 'MDLE');
|
const tokenSymbol = computed(() => selectedDle.value?.symbol || '');
|
||||||
const totalSupply = computed(() => selectedDle.value?.initialAmounts?.[0] || 10000);
|
const totalSupply = computed(() => selectedDle.value?.totalSupply || 0);
|
||||||
const userBalance = computed(() => Math.floor(totalSupply.value * 0.1)); // 10% для демо
|
const userBalance = computed(() => selectedDle.value?.deployerBalance || 0);
|
||||||
const quorumPercentage = computed(() => selectedDle.value?.governanceSettings?.quorumPercentage || 51);
|
const quorumPercentage = computed(() => selectedDle.value?.quorumPercentage || 0);
|
||||||
const tokenPrice = ref(1.25);
|
const tokenPrice = ref(0);
|
||||||
|
|
||||||
// Данные трансфера
|
// Данные трансфера
|
||||||
const transferData = ref({
|
const transferData = ref({
|
||||||
@@ -250,14 +250,8 @@ const distributionData = ref({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Держатели токенов (временные данные)
|
// Держатели токенов (загружаются из блокчейна)
|
||||||
const tokenHolders = ref([
|
const tokenHolders = ref([]);
|
||||||
{ address: '0x1234567890123456789012345678901234567890', balance: 2500 },
|
|
||||||
{ address: '0x2345678901234567890123456789012345678901', balance: 1800 },
|
|
||||||
{ address: '0x3456789012345678901234567890123456789012', balance: 1200 },
|
|
||||||
{ address: '0x4567890123456789012345678901234567890123', balance: 800 },
|
|
||||||
{ address: '0x5678901234567890123456789012345678901234', balance: 600 }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Функции
|
// Функции
|
||||||
async function loadDleData() {
|
async function loadDleData() {
|
||||||
@@ -268,27 +262,38 @@ async function loadDleData() {
|
|||||||
|
|
||||||
isLoadingDle.value = true;
|
isLoadingDle.value = true;
|
||||||
try {
|
try {
|
||||||
// Загружаем данные DLE из backend
|
// Читаем актуальные данные из блокчейна
|
||||||
const response = await axios.get(`/dle-v2`);
|
const blockchainResponse = await axios.post('/blockchain/read-dle-info', {
|
||||||
const dles = response.data.data; // Используем response.data.data
|
dleAddress: dleAddress.value
|
||||||
|
});
|
||||||
|
|
||||||
// Находим нужный DLE по адресу
|
if (blockchainResponse.data.success) {
|
||||||
const dle = dles.find(d => d.dleAddress === dleAddress.value);
|
const blockchainData = blockchainResponse.data.data;
|
||||||
|
selectedDle.value = blockchainData;
|
||||||
|
console.log('Загружены данные DLE из блокчейна:', blockchainData);
|
||||||
|
|
||||||
if (dle) {
|
// Загружаем держателей токенов (если есть API)
|
||||||
selectedDle.value = dle;
|
await loadTokenHolders();
|
||||||
console.log('Загружен DLE:', dle);
|
|
||||||
console.log('Данные токенов будут обновлены автоматически');
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('DLE не найден:', dleAddress.value);
|
console.warn('Не удалось прочитать данные из блокчейна для', dleAddress.value);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки DLE:', error);
|
console.error('Ошибка загрузки данных DLE из блокчейна:', error);
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingDle.value = false;
|
isLoadingDle.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTokenHolders() {
|
||||||
|
try {
|
||||||
|
// Здесь можно добавить загрузку держателей токенов из блокчейна
|
||||||
|
// Пока оставляем пустым
|
||||||
|
tokenHolders.value = [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки держателей токенов:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function shortenAddress(address) {
|
function shortenAddress(address) {
|
||||||
if (!address) return '';
|
if (!address) return '';
|
||||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
|||||||
@@ -270,49 +270,8 @@ const dailyChange = ref(25000);
|
|||||||
const assetsCount = ref(5);
|
const assetsCount = ref(5);
|
||||||
const yieldPercentage = ref(8.5);
|
const yieldPercentage = ref(8.5);
|
||||||
|
|
||||||
// Активы (временные данные)
|
// Активы (загружаются из блокчейна)
|
||||||
const assets = ref([
|
const assets = ref([]);
|
||||||
{
|
|
||||||
id: 'eth',
|
|
||||||
name: 'Ethereum',
|
|
||||||
symbol: 'ETH',
|
|
||||||
balance: 125.5,
|
|
||||||
value: 450000,
|
|
||||||
change: 2.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'usdc',
|
|
||||||
name: 'USD Coin',
|
|
||||||
symbol: 'USDC',
|
|
||||||
balance: 500000,
|
|
||||||
value: 500000,
|
|
||||||
change: 0.1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'btc',
|
|
||||||
name: 'Bitcoin',
|
|
||||||
symbol: 'BTC',
|
|
||||||
balance: 2.5,
|
|
||||||
value: 150000,
|
|
||||||
change: -1.2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'matic',
|
|
||||||
name: 'Polygon',
|
|
||||||
symbol: 'MATIC',
|
|
||||||
balance: 50000,
|
|
||||||
value: 75000,
|
|
||||||
change: 5.8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'link',
|
|
||||||
name: 'Chainlink',
|
|
||||||
symbol: 'LINK',
|
|
||||||
balance: 2500,
|
|
||||||
value: 75000,
|
|
||||||
change: 3.2
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Вкладки операций
|
// Вкладки операций
|
||||||
const operationTabs = ref([
|
const operationTabs = ref([
|
||||||
@@ -335,32 +294,8 @@ const withdrawData = ref({
|
|||||||
reason: ''
|
reason: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// История операций (временные данные)
|
// История операций (загружается из блокчейна)
|
||||||
const operationsHistory = ref([
|
const operationsHistory = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
type: 'deposit',
|
|
||||||
asset: 'Ethereum',
|
|
||||||
symbol: 'ETH',
|
|
||||||
amount: 10.5,
|
|
||||||
value: 37500,
|
|
||||||
reason: 'Пополнение казны от доходов',
|
|
||||||
timestamp: Date.now() - 3600000,
|
|
||||||
status: 'completed'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
type: 'withdraw',
|
|
||||||
asset: 'USD Coin',
|
|
||||||
symbol: 'USDC',
|
|
||||||
amount: 25000,
|
|
||||||
value: 25000,
|
|
||||||
reason: 'Выплата партнерам',
|
|
||||||
recipient: '0x1234567890123456789012345678901234567890',
|
|
||||||
timestamp: Date.now() - 7200000,
|
|
||||||
status: 'completed'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Методы
|
// Методы
|
||||||
const depositAsset = (asset) => {
|
const depositAsset = (asset) => {
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ block_ip() {
|
|||||||
local ip=$1
|
local ip=$1
|
||||||
local reason=$2
|
local reason=$2
|
||||||
|
|
||||||
|
# Исключаем внутренние Docker IP адреса
|
||||||
|
if [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]] || [[ "$ip" =~ ^10\. ]] || [[ "$ip" =~ ^192\.168\. ]]; then
|
||||||
|
echo "🔒 Пропускаем внутренний IP: $ip (причина: $reason)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
# Проверяем, не заблокирован ли уже IP
|
# Проверяем, не заблокирован ли уже IP
|
||||||
if grep -q "^$ip$" "$BLOCKED_IPS_FILE"; then
|
if grep -q "^$ip$" "$BLOCKED_IPS_FILE"; then
|
||||||
return
|
return
|
||||||
@@ -91,11 +97,25 @@ analyze_logs() {
|
|||||||
block_ip "$ip" "Попытка доступа к чувствительным файлам"
|
block_ip "$ip" "Попытка доступа к чувствительным файлам"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Проверяем на сканирование резервных копий и архивов
|
||||||
|
if echo "$line" | grep -q "backup\|backups\|bak\|old\|restore\|\.tar\|\.gz\|sftp-config"; then
|
||||||
|
block_ip "$ip" "Сканирование резервных копий и конфигурационных файлов"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Проверяем на подозрительные поддомены
|
||||||
|
if echo "$line" | grep -q "bestcupcakerecipes\|usmc1\|test\|admin\|dev\|staging"; then
|
||||||
|
block_ip "$ip" "Попытка доступа к несуществующим поддоменам"
|
||||||
|
fi
|
||||||
|
|
||||||
# Проверяем на старые User-Agent
|
# Проверяем на старые User-Agent
|
||||||
if echo "$line" | grep -q "Chrome/[1-7][0-9]\."; then
|
if echo "$line" | grep -q "Chrome/[1-7][0-9]\."; then
|
||||||
block_ip "$ip" "Подозрительный User-Agent (старый Chrome)"
|
block_ip "$ip" "Подозрительный User-Agent (старый Chrome)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if echo "$line" | grep -q "Safari/[1-5][0-9][0-9]\."; then
|
||||||
|
block_ip "$ip" "Подозрительный User-Agent (старый Safari)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Проверяем на известные сканеры
|
# Проверяем на известные сканеры
|
||||||
if echo "$line" | grep -qi "bot\|crawler\|spider\|scanner\|nmap\|sqlmap"; then
|
if echo "$line" | grep -qi "bot\|crawler\|spider\|scanner\|nmap\|sqlmap"; then
|
||||||
block_ip "$ip" "Известный сканер/бот"
|
block_ip "$ip" "Известный сканер/бот"
|
||||||
|
|||||||
Reference in New Issue
Block a user