ваше сообщение коммита
This commit is contained in:
106
frontend/src/utils/dle-abi.js
Normal file
106
frontend/src/utils/dle-abi.js
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* ABI для DLE смарт-контракта
|
||||
* АВТОМАТИЧЕСКИ СГЕНЕРИРОВАНО - НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
|
||||
* Для обновления запустите: node backend/scripts/generate-abi.js
|
||||
*
|
||||
* Последнее обновление: 2025-09-29T18:16:32.027Z
|
||||
*/
|
||||
|
||||
export const DLE_ABI = [
|
||||
"function CLOCK_MODE() returns (string)",
|
||||
"function DOMAIN_SEPARATOR() returns (bytes32)",
|
||||
"function activeModules(bytes32 ) returns (bool)",
|
||||
"function allProposalIds(uint256 ) returns (uint256)",
|
||||
"function allowance(address owner, address spender) returns (uint256)",
|
||||
"function approve(address , uint256 ) returns (bool)",
|
||||
"function balanceOf(address account) returns (uint256)",
|
||||
"function cancelProposal(uint256 _proposalId, string reason)",
|
||||
"function checkProposalResult(uint256 _proposalId) returns (bool, bool)",
|
||||
"function checkpoints(address account, uint32 pos) returns (tuple)",
|
||||
"function clock() returns (uint48)",
|
||||
"function createAddModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, address _moduleAddress, uint256 _chainId) returns (uint256)",
|
||||
"function createProposal(string _description, uint256 _duration, bytes _operation, uint256 _governanceChainId, uint256[] _targetChains, uint256 ) returns (uint256)",
|
||||
"function createRemoveModuleProposal(string _description, uint256 _duration, bytes32 _moduleId, uint256 _chainId) returns (uint256)",
|
||||
"function decimals() returns (uint8)",
|
||||
"function delegate(address delegatee)",
|
||||
"function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)",
|
||||
"function delegates(address account) returns (address)",
|
||||
"function dleInfo() returns (string, string, string, string, uint256, uint256, uint256, bool)",
|
||||
"function eip712Domain() returns (bytes1, string, string, uint256, address, bytes32, uint256[])",
|
||||
"function executeProposal(uint256 _proposalId)",
|
||||
"function executeProposalBySignatures(uint256 _proposalId, address[] signers, bytes[] signatures)",
|
||||
"function getCurrentChainId() returns (uint256)",
|
||||
"function getDLEInfo() returns (tuple)",
|
||||
"function getModuleAddress(bytes32 _moduleId) returns (address)",
|
||||
"function getMultichainAddresses() returns (uint256[], address[])",
|
||||
"function getMultichainInfo() returns (uint256[], uint256)",
|
||||
"function getMultichainMetadata() returns (string)",
|
||||
"function getPastTotalSupply(uint256 timepoint) returns (uint256)",
|
||||
"function getPastVotes(address account, uint256 timepoint) returns (uint256)",
|
||||
"function getProposalState(uint256 _proposalId) returns (uint8)",
|
||||
"function getProposalSummary(uint256 _proposalId) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, uint256, uint256, uint256[])",
|
||||
"function getSupportedChainCount() returns (uint256)",
|
||||
"function getSupportedChainId(uint256 _index) returns (uint256)",
|
||||
"function getVotes(address account) returns (uint256)",
|
||||
"function initializeLogoURI(string _logoURI)",
|
||||
"function initializer() returns (address)",
|
||||
"function isActive() returns (bool)",
|
||||
"function isChainSupported(uint256 _chainId) returns (bool)",
|
||||
"function isModuleActive(bytes32 _moduleId) returns (bool)",
|
||||
"function logo() returns (string)",
|
||||
"function logoURI() returns (string)",
|
||||
"function maxVotingDuration() returns (uint256)",
|
||||
"function minVotingDuration() returns (uint256)",
|
||||
"function modules(bytes32 ) returns (address)",
|
||||
"function name() returns (string)",
|
||||
"function nonces(address owner) returns (uint256)",
|
||||
"function numCheckpoints(address account) returns (uint32)",
|
||||
"function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)",
|
||||
"function proposalCounter() returns (uint256)",
|
||||
"function proposals(uint256 ) returns (uint256, string, uint256, uint256, bool, bool, uint256, address, bytes, uint256, uint256)",
|
||||
"function quorumPercentage() returns (uint256)",
|
||||
"function supportedChainIds(uint256 ) returns (uint256)",
|
||||
"function supportedChains(uint256 ) returns (bool)",
|
||||
"function symbol() returns (string)",
|
||||
"function tokenURI() returns (string)",
|
||||
"function totalSupply() returns (uint256)",
|
||||
"function transfer(address , uint256 ) returns (bool)",
|
||||
"function transferFrom(address , address , uint256 ) returns (bool)",
|
||||
"function vote(uint256 _proposalId, bool _support)",
|
||||
"event Approval(address owner, address spender, uint256 value)",
|
||||
"event ChainAdded(uint256 chainId)",
|
||||
"event ChainRemoved(uint256 chainId)",
|
||||
"event DLEInfoUpdated(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp)",
|
||||
"event DLEInitialized(string name, string symbol, string location, string coordinates, uint256 jurisdiction, string[] okvedCodes, uint256 kpp, address tokenAddress, uint256[] supportedChainIds)",
|
||||
"event DelegateChanged(address delegator, address fromDelegate, address toDelegate)",
|
||||
"event DelegateVotesChanged(address delegate, uint256 previousVotes, uint256 newVotes)",
|
||||
"event EIP712DomainChanged()",
|
||||
"event InitialTokensDistributed(address[] partners, uint256[] amounts)",
|
||||
"event LogoURIUpdated(string oldURI, string newURI)",
|
||||
"event ModuleAdded(bytes32 moduleId, address moduleAddress)",
|
||||
"event ModuleRemoved(bytes32 moduleId)",
|
||||
"event ProposalCancelled(uint256 proposalId, string reason)",
|
||||
"event ProposalCreated(uint256 proposalId, address initiator, string description)",
|
||||
"event ProposalExecuted(uint256 proposalId, bytes operation)",
|
||||
"event ProposalExecutionApprovedInChain(uint256 proposalId, uint256 chainId)",
|
||||
"event ProposalGovernanceChainSet(uint256 proposalId, uint256 governanceChainId)",
|
||||
"event ProposalTargetsSet(uint256 proposalId, uint256[] targetChains)",
|
||||
"event ProposalVoted(uint256 proposalId, address voter, bool support, uint256 votingPower)",
|
||||
"event QuorumPercentageUpdated(uint256 oldQuorumPercentage, uint256 newQuorumPercentage)",
|
||||
"event TokensTransferredByGovernance(address recipient, uint256 amount)",
|
||||
"event Transfer(address from, address to, uint256 value)",
|
||||
"event VotingDurationsUpdated(uint256 oldMinDuration, uint256 newMinDuration, uint256 oldMaxDuration, uint256 newMaxDuration)",
|
||||
];
|
||||
|
||||
|
||||
// ABI для деактивации (специальные функции) - НЕ СУЩЕСТВУЮТ В КОНТРАКТЕ
|
||||
export const DLE_DEACTIVATION_ABI = [
|
||||
// Эти функции не существуют в контракте DLE
|
||||
];
|
||||
|
||||
// ABI для токенов (базовые функции)
|
||||
export const TOKEN_ABI = [
|
||||
"function balanceOf(address owner) view returns (uint256)",
|
||||
"function decimals() view returns (uint8)",
|
||||
"function totalSupply() view returns (uint256)"
|
||||
];
|
||||
@@ -12,6 +12,91 @@
|
||||
|
||||
import api from '@/api/axios';
|
||||
import { ethers } from 'ethers';
|
||||
import { DLE_ABI, DLE_DEACTIVATION_ABI, TOKEN_ABI } from './dle-abi';
|
||||
|
||||
// Функция для переключения сети кошелька
|
||||
export async function switchToVotingNetwork(chainId) {
|
||||
try {
|
||||
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId}...`);
|
||||
|
||||
// Конфигурации сетей
|
||||
const networks = {
|
||||
'11155111': { // Sepolia
|
||||
chainId: '0xaa36a7',
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://1rpc.io/sepolia'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io']
|
||||
},
|
||||
'17000': { // Holesky
|
||||
chainId: '0x4268',
|
||||
chainName: 'Holesky',
|
||||
nativeCurrency: { name: 'Holesky Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://ethereum-holesky.publicnode.com'],
|
||||
blockExplorerUrls: ['https://holesky.etherscan.io']
|
||||
},
|
||||
'421614': { // Arbitrum Sepolia
|
||||
chainId: '0x66eee',
|
||||
chainName: 'Arbitrum Sepolia',
|
||||
nativeCurrency: { name: 'Arbitrum Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
|
||||
blockExplorerUrls: ['https://sepolia.arbiscan.io']
|
||||
},
|
||||
'84532': { // Base Sepolia
|
||||
chainId: '0x14a34',
|
||||
chainName: 'Base Sepolia',
|
||||
nativeCurrency: { name: 'Base Sepolia Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrls: ['https://sepolia.base.org'],
|
||||
blockExplorerUrls: ['https://sepolia.basescan.org']
|
||||
}
|
||||
};
|
||||
|
||||
const networkConfig = networks[chainId];
|
||||
if (!networkConfig) {
|
||||
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем, подключена ли уже нужная сеть
|
||||
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
if (currentChainId === networkConfig.chainId) {
|
||||
console.log(`✅ [NETWORK] Сеть ${chainId} уже подключена`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Пытаемся переключиться на нужную сеть
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: networkConfig.chainId }]
|
||||
});
|
||||
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainId}`);
|
||||
return true;
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log(`➕ [NETWORK] Добавляем сеть ${chainId}...`);
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [networkConfig]
|
||||
});
|
||||
console.log(`✅ [NETWORK] Сеть ${chainId} добавлена и подключена`);
|
||||
return true;
|
||||
} catch (addError) {
|
||||
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainId}:`, addError);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainId}:`, switchError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [NETWORK] Общая ошибка переключения сети:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить подключение к браузерному кошельку
|
||||
@@ -60,6 +145,8 @@ export async function checkWalletConnection() {
|
||||
* Используется только система голосования (proposals)
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Получить информацию о DLE из блокчейна
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
@@ -109,12 +196,9 @@ export async function createProposal(dleAddress, proposalData) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для создания предложения
|
||||
const dleAbi = [
|
||||
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
|
||||
];
|
||||
// Используем общий ABI
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Создаем предложение
|
||||
const tx = await dle.createProposal(
|
||||
@@ -162,14 +246,111 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для голосования
|
||||
const dleAbi = [
|
||||
"function vote(uint256 _proposalId, bool _support) external"
|
||||
];
|
||||
// Используем общий ABI
|
||||
let dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
// Дополнительная диагностика перед голосованием
|
||||
try {
|
||||
console.log('🔍 [VOTE DEBUG] Проверяем состояние предложения...');
|
||||
const proposalState = await dle.getProposalState(proposalId);
|
||||
console.log('🔍 [VOTE DEBUG] Состояние предложения:', proposalState);
|
||||
|
||||
// Проверяем, можно ли голосовать (состояние должно быть 0 = Pending)
|
||||
if (Number(proposalState) !== 0) {
|
||||
throw new Error(`Предложение в состоянии ${proposalState}, голосование невозможно`);
|
||||
}
|
||||
|
||||
console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования');
|
||||
|
||||
// Проверяем сеть голосования
|
||||
try {
|
||||
const proposal = await dle.proposals(proposalId);
|
||||
const currentChainId = await dle.getCurrentChainId();
|
||||
const governanceChainId = proposal.governanceChainId;
|
||||
|
||||
console.log('🔍 [VOTE DEBUG] Текущая сеть контракта:', currentChainId.toString());
|
||||
console.log('🔍 [VOTE DEBUG] Сеть голосования предложения:', governanceChainId.toString());
|
||||
|
||||
if (currentChainId.toString() !== governanceChainId.toString()) {
|
||||
console.log('🔄 [VOTE DEBUG] Неправильная сеть! Пытаемся переключиться...');
|
||||
|
||||
// Пытаемся переключить сеть
|
||||
const switched = await switchToVotingNetwork(governanceChainId.toString());
|
||||
if (switched) {
|
||||
console.log('✅ [VOTE DEBUG] Сеть успешно переключена, переподключаемся к контракту...');
|
||||
|
||||
// Определяем правильный адрес контракта для сети голосования
|
||||
let correctContractAddress = dleAddress;
|
||||
|
||||
// Если контракт развернут в другой сети, нужно найти контракт в нужной сети
|
||||
if (currentChainId.toString() !== governanceChainId.toString()) {
|
||||
console.log('🔍 [VOTE DEBUG] Ищем контракт в сети голосования...');
|
||||
|
||||
try {
|
||||
// Получаем информацию о мультичейн развертывании из БД
|
||||
const response = await fetch('/api/dle-core/get-multichain-contracts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
originalContract: dleAddress,
|
||||
targetChainId: governanceChainId.toString()
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.contractAddress) {
|
||||
correctContractAddress = data.contractAddress;
|
||||
console.log('🔍 [VOTE DEBUG] Найден контракт в сети голосования:', correctContractAddress);
|
||||
} else {
|
||||
console.warn('⚠️ [VOTE DEBUG] Контракт в сети голосования не найден, используем исходный');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ [VOTE DEBUG] Ошибка получения контракта из БД, используем исходный');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ [VOTE DEBUG] Ошибка поиска контракта, используем исходный:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Переподключаемся к контракту в новой сети
|
||||
const newProvider = new ethers.BrowserProvider(window.ethereum);
|
||||
const newSigner = await newProvider.getSigner();
|
||||
dle = new ethers.Contract(correctContractAddress, DLE_ABI, newSigner);
|
||||
|
||||
// Проверяем, что теперь все корректно
|
||||
const newCurrentChainId = await dle.getCurrentChainId();
|
||||
console.log('🔍 [VOTE DEBUG] Новая текущая сеть контракта:', newCurrentChainId.toString());
|
||||
|
||||
if (newCurrentChainId.toString() === governanceChainId.toString()) {
|
||||
console.log('✅ [VOTE DEBUG] Сеть для голосования теперь корректна');
|
||||
} else {
|
||||
throw new Error(`Не удалось переключиться на правильную сеть. Текущая: ${newCurrentChainId}, требуется: ${governanceChainId}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Неправильная сеть! Контракт в сети ${currentChainId}, а голосование должно быть в сети ${governanceChainId}. Переключите кошелек вручную.`);
|
||||
}
|
||||
} else {
|
||||
console.log('🔍 [VOTE DEBUG] Сеть для голосования корректна');
|
||||
}
|
||||
|
||||
// Проверяем право голоса
|
||||
const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint);
|
||||
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
|
||||
if (votingPower === 0n) {
|
||||
throw new Error('У пользователя нет права голоса (votingPower = 0)');
|
||||
}
|
||||
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
|
||||
} catch (votingPowerError) {
|
||||
console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message);
|
||||
}
|
||||
|
||||
} catch (debugError) {
|
||||
console.warn('⚠️ [VOTE DEBUG] Ошибка диагностики (продолжаем):', debugError.message);
|
||||
}
|
||||
|
||||
// Голосуем за предложение
|
||||
console.log('🗳️ [VOTE] Отправляем транзакцию голосования...');
|
||||
const tx = await dle.vote(proposalId, support);
|
||||
|
||||
// Ждем подтверждения транзакции
|
||||
@@ -182,10 +363,40 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
||||
blockNumber: receipt.blockNumber
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка голосования:', error);
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка голосования:', error);
|
||||
|
||||
// Детальная диагностика ошибки
|
||||
if (error.code === 'CALL_EXCEPTION' && error.data) {
|
||||
console.error('🔍 [ERROR DEBUG] Детали ошибки:', {
|
||||
code: error.code,
|
||||
data: error.data,
|
||||
reason: error.reason,
|
||||
action: error.action
|
||||
});
|
||||
|
||||
// Расшифровка кода ошибки
|
||||
if (error.data === '0x2eaf0f6d') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrWrongChain - неправильная сеть для голосования');
|
||||
} else if (error.data === '0xe7005635') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrAlreadyVoted - пользователь уже голосовал по этому предложению');
|
||||
} else if (error.data === '0x21c19873') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrNoPower - у пользователя нет права голоса');
|
||||
} else if (error.data === '0x834d7b85') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalMissing - предложение не найдено');
|
||||
} else if (error.data === '0xd6792fad') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalEnded - время голосования истекло');
|
||||
} else if (error.data === '0x2d686f73') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalExecuted - предложение уже исполнено');
|
||||
} else if (error.data === '0xc7567e07') {
|
||||
console.error('❌ [ERROR DEBUG] Ошибка: ErrProposalCanceled - предложение отменено');
|
||||
} else {
|
||||
console.error('❌ [ERROR DEBUG] Неизвестная ошибка:', error.data);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,12 +417,9 @@ export async function executeProposal(dleAddress, proposalId) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для исполнения предложения
|
||||
const dleAbi = [
|
||||
"function executeProposal(uint256 _proposalId) external"
|
||||
];
|
||||
// Используем общий ABI
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Исполняем предложение
|
||||
const tx = await dle.executeProposal(proposalId);
|
||||
@@ -233,30 +441,112 @@ export async function executeProposal(dleAddress, proposalId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать предложение о добавлении модуля
|
||||
* Отменить предложение
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {string} reason - Причина отмены
|
||||
* @returns {Promise<Object>} - Результат отмены
|
||||
*/
|
||||
export async function cancelProposal(dleAddress, proposalId, reason) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
if (!window.ethereum) {
|
||||
throw new Error('Браузерный кошелек не установлен');
|
||||
}
|
||||
|
||||
// Запрашиваем подключение к кошельку
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// Используем общий ABI
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Отменяем предложение
|
||||
const tx = await dle.cancelProposal(proposalId, reason);
|
||||
|
||||
// Ждем подтверждения транзакции
|
||||
const receipt = await tx.wait();
|
||||
|
||||
console.log('Предложение отменено, tx hash:', tx.hash);
|
||||
|
||||
return {
|
||||
txHash: tx.hash,
|
||||
blockNumber: receipt.blockNumber
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Ошибка отмены предложения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить баланс токенов пользователя
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Баланс токенов
|
||||
*/
|
||||
export async function checkTokenBalance(dleAddress, userAddress) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
if (!window.ethereum) {
|
||||
throw new Error('Браузерный кошелек не установлен');
|
||||
}
|
||||
|
||||
// Создаем провайдер (только для чтения)
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, provider);
|
||||
|
||||
// Получаем баланс токенов
|
||||
const balance = await dle.balanceOf(userAddress);
|
||||
const balanceFormatted = ethers.formatEther(balance);
|
||||
|
||||
console.log(`💰 Баланс токенов для ${userAddress}: ${balanceFormatted}`);
|
||||
|
||||
return {
|
||||
balance: balanceFormatted,
|
||||
hasTokens: balance > 0,
|
||||
rawBalance: balance.toString()
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Ошибка проверки баланса токенов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создать предложение о добавлении модуля (с автоматической оплатой газа)
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {string} description - Описание предложения
|
||||
* @param {number} duration - Длительность голосования в секундах
|
||||
* @param {string} moduleId - ID модуля
|
||||
* @param {string} moduleAddress - Адрес модуля
|
||||
* @param {number} chainId - ID цепочки для голосования
|
||||
* @param {string} deploymentId - ID деплоя для получения приватного ключа (опционально)
|
||||
* @returns {Promise<Object>} - Результат создания предложения
|
||||
*/
|
||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
|
||||
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId, deploymentId = null) {
|
||||
try {
|
||||
const response = await api.post('/blockchain/create-add-module-proposal', {
|
||||
const requestData = {
|
||||
dleAddress: dleAddress,
|
||||
description: description,
|
||||
duration: duration,
|
||||
moduleId: moduleId,
|
||||
moduleAddress: moduleAddress,
|
||||
chainId: chainId
|
||||
});
|
||||
};
|
||||
|
||||
// Добавляем deploymentId если он передан
|
||||
if (deploymentId) {
|
||||
requestData.deploymentId = deploymentId;
|
||||
}
|
||||
|
||||
const response = await api.post('/dle-modules/create-add-module-proposal', requestData);
|
||||
|
||||
if (response.data.success) {
|
||||
return response.data.data;
|
||||
} else {
|
||||
throw new Error(response.data.message || 'Не удалось создать предложение о добавлении модуля');
|
||||
throw new Error(response.data.error || 'Не удалось создать предложение о добавлении модуля');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка создания предложения о добавлении модуля:', error);
|
||||
@@ -537,6 +827,7 @@ export async function getSupportedChains(dleAddress) {
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат деактивации
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function deactivateDLE(dleAddress, userAddress) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
@@ -568,15 +859,9 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
||||
|
||||
console.log('Проверка деактивации прошла успешно, выполняем деактивацию...');
|
||||
|
||||
// ABI для деактивации DLE
|
||||
const dleAbi = [
|
||||
"function deactivate() external",
|
||||
"function balanceOf(address) external view returns (uint256)",
|
||||
"function totalSupply() external view returns (uint256)",
|
||||
"function isActive() external view returns (bool)"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Дополнительные проверки перед деактивацией
|
||||
const balance = await dle.balanceOf(userAddress);
|
||||
@@ -640,6 +925,7 @@ export async function deactivateDLE(dleAddress, userAddress) {
|
||||
* @param {number} chainId - ID цепочки для деактивации
|
||||
* @returns {Promise<Object>} - Результат создания предложения
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function createDeactivationProposal(dleAddress, description, duration, chainId) {
|
||||
try {
|
||||
// Проверяем наличие браузерного кошелька
|
||||
@@ -650,11 +936,9 @@ export async function createDeactivationProposal(dleAddress, description, durati
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const dleAbi = [
|
||||
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||
|
||||
const tx = await dle.createDeactivationProposal(description, duration, chainId);
|
||||
const receipt = await tx.wait();
|
||||
@@ -681,6 +965,7 @@ export async function createDeactivationProposal(dleAddress, description, durati
|
||||
* @param {boolean} support - Поддержка предложения
|
||||
* @returns {Promise<Object>} - Результат голосования
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function voteDeactivationProposal(dleAddress, proposalId, support) {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
@@ -690,11 +975,9 @@ export async function voteDeactivationProposal(dleAddress, proposalId, support)
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const dleAbi = [
|
||||
"function voteDeactivation(uint256 _proposalId, bool _support) external"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||
|
||||
const tx = await dle.voteDeactivation(proposalId, support);
|
||||
const receipt = await tx.wait();
|
||||
@@ -744,6 +1027,7 @@ export async function checkDeactivationProposalResult(dleAddress, proposalId) {
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
// ФУНКЦИЯ НЕ СУЩЕСТВУЕТ В КОНТРАКТЕ
|
||||
export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
@@ -753,11 +1037,9 @@ export async function executeDeactivationProposal(dleAddress, proposalId) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
const dleAbi = [
|
||||
"function executeDeactivationProposal(uint256 _proposalId) external"
|
||||
];
|
||||
// Используем общий ABI для деактивации
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_DEACTIVATION_ABI, signer);
|
||||
|
||||
const tx = await dle.executeDeactivationProposal(proposalId);
|
||||
const receipt = await tx.wait();
|
||||
@@ -823,12 +1105,9 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
||||
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||
const signer = await provider.getSigner();
|
||||
|
||||
// ABI для создания предложения
|
||||
const dleAbi = [
|
||||
"function createProposal(string memory _description, uint256 _duration, bytes memory _operation, uint256 _governanceChainId, uint256[] memory _targetChains, uint256 _timelockDelay) external returns (uint256)"
|
||||
];
|
||||
// Используем общий ABI
|
||||
|
||||
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
|
||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||
|
||||
// Кодируем операцию перевода токенов
|
||||
const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)");
|
||||
@@ -872,4 +1151,75 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
||||
console.error('Ошибка создания предложения о переводе токенов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исполнить мультиконтрактное предложение во всех целевых сетях
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export async function executeMultichainProposal(dleAddress, proposalId, userAddress) {
|
||||
try {
|
||||
// Импортируем сервис мультиконтрактного исполнения
|
||||
const {
|
||||
executeInAllTargetChains,
|
||||
getDeploymentId,
|
||||
formatExecutionResult,
|
||||
getExecutionErrors
|
||||
} = await import('@/services/multichainExecutionService');
|
||||
|
||||
// Получаем ID деплоя
|
||||
const deploymentId = await getDeploymentId(dleAddress);
|
||||
|
||||
// Исполняем во всех целевых сетях
|
||||
const result = await executeInAllTargetChains(dleAddress, proposalId, deploymentId, userAddress);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
summary: formatExecutionResult(result),
|
||||
errors: getExecutionErrors(result)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка исполнения мультиконтрактного предложения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Исполнить мультиконтрактное предложение в конкретной сети
|
||||
* @param {string} dleAddress - Адрес DLE контракта
|
||||
* @param {number} proposalId - ID предложения
|
||||
* @param {number} targetChainId - ID целевой сети
|
||||
* @param {string} userAddress - Адрес пользователя
|
||||
* @returns {Promise<Object>} - Результат исполнения
|
||||
*/
|
||||
export async function executeMultichainProposalInChain(dleAddress, proposalId, targetChainId, userAddress) {
|
||||
try {
|
||||
// Импортируем сервис мультиконтрактного исполнения
|
||||
const {
|
||||
executeInTargetChain,
|
||||
getDeploymentId,
|
||||
getChainName
|
||||
} = await import('@/services/multichainExecutionService');
|
||||
|
||||
// Получаем ID деплоя
|
||||
const deploymentId = await getDeploymentId(dleAddress);
|
||||
|
||||
// Исполняем в конкретной сети
|
||||
const result = await executeInTargetChain(dleAddress, proposalId, targetChainId, deploymentId, userAddress);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
chainName: getChainName(targetChainId)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка исполнения мультиконтрактного предложения в сети:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
105
frontend/src/utils/networkConfig.js
Normal file
105
frontend/src/utils/networkConfig.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Конфигурации сетей блокчейна для DLE
|
||||
*
|
||||
* Author: HB3 Accelerator
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
export const SUPPORTED_NETWORKS = {
|
||||
1: {
|
||||
chainId: '0x1',
|
||||
chainName: 'Ethereum Mainnet',
|
||||
nativeCurrency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://mainnet.infura.io/v3/'],
|
||||
blockExplorerUrls: ['https://etherscan.io'],
|
||||
},
|
||||
11155111: {
|
||||
chainId: '0xaa36a7',
|
||||
chainName: 'Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Sepolia Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://sepolia.infura.io/v3/', 'https://1rpc.io/sepolia'],
|
||||
blockExplorerUrls: ['https://sepolia.etherscan.io'],
|
||||
},
|
||||
17000: {
|
||||
chainId: '0x4268',
|
||||
chainName: 'Holesky',
|
||||
nativeCurrency: {
|
||||
name: 'Holesky Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://ethereum-holesky.publicnode.com'],
|
||||
blockExplorerUrls: ['https://holesky.etherscan.io'],
|
||||
},
|
||||
421614: {
|
||||
chainId: '0x66eee',
|
||||
chainName: 'Arbitrum Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Arbitrum Sepolia Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://sepolia-rollup.arbitrum.io/rpc'],
|
||||
blockExplorerUrls: ['https://sepolia.arbiscan.io'],
|
||||
},
|
||||
84532: {
|
||||
chainId: '0x14a34',
|
||||
chainName: 'Base Sepolia',
|
||||
nativeCurrency: {
|
||||
name: 'Base Sepolia Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://sepolia.base.org'],
|
||||
blockExplorerUrls: ['https://sepolia.basescan.org'],
|
||||
},
|
||||
8453: {
|
||||
chainId: '0x2105',
|
||||
chainName: 'Base',
|
||||
nativeCurrency: {
|
||||
name: 'Base Ether',
|
||||
symbol: 'ETH',
|
||||
decimals: 18,
|
||||
},
|
||||
rpcUrls: ['https://mainnet.base.org'],
|
||||
blockExplorerUrls: ['https://basescan.org'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Получить конфигурацию сети по chainId
|
||||
* @param {number|string} chainId - ID сети
|
||||
* @returns {Object|null} - Конфигурация сети или null
|
||||
*/
|
||||
export function getNetworkConfig(chainId) {
|
||||
const numericChainId = typeof chainId === 'string' ? parseInt(chainId, 16) : chainId;
|
||||
return SUPPORTED_NETWORKS[numericChainId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить hex представление chainId
|
||||
* @param {number} chainId - ID сети
|
||||
* @returns {string} - Hex представление
|
||||
*/
|
||||
export function getHexChainId(chainId) {
|
||||
return `0x${chainId.toString(16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, поддерживается ли сеть
|
||||
* @param {number|string} chainId - ID сети
|
||||
* @returns {boolean} - Поддерживается ли сеть
|
||||
*/
|
||||
export function isNetworkSupported(chainId) {
|
||||
return getNetworkConfig(chainId) !== null;
|
||||
}
|
||||
157
frontend/src/utils/networkSwitcher.js
Normal file
157
frontend/src/utils/networkSwitcher.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Утилиты для переключения сетей блокчейна
|
||||
*
|
||||
* Author: HB3 Accelerator
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
import { getNetworkConfig, getHexChainId, isNetworkSupported } from './networkConfig.js';
|
||||
|
||||
/**
|
||||
* Переключить сеть в MetaMask
|
||||
* @param {number} targetChainId - ID целевой сети
|
||||
* @returns {Promise<Object>} - Результат переключения
|
||||
*/
|
||||
export async function switchNetwork(targetChainId) {
|
||||
try {
|
||||
console.log(`🔄 [Network Switch] Переключаемся на сеть ${targetChainId}...`);
|
||||
|
||||
// Проверяем, поддерживается ли сеть
|
||||
if (!isNetworkSupported(targetChainId)) {
|
||||
throw new Error(`Сеть ${targetChainId} не поддерживается`);
|
||||
}
|
||||
|
||||
// Проверяем наличие MetaMask
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден. Пожалуйста, установите MetaMask.');
|
||||
}
|
||||
|
||||
// Получаем конфигурацию сети
|
||||
const networkConfig = getNetworkConfig(targetChainId);
|
||||
if (!networkConfig) {
|
||||
throw new Error(`Конфигурация для сети ${targetChainId} не найдена`);
|
||||
}
|
||||
|
||||
// Проверяем текущую сеть
|
||||
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
console.log(`🔄 [Network Switch] Текущая сеть: ${currentChainId}, Целевая: ${getHexChainId(targetChainId)}`);
|
||||
|
||||
// Если уже в нужной сети, возвращаем успех
|
||||
if (currentChainId === getHexChainId(targetChainId)) {
|
||||
console.log(`✅ [Network Switch] Уже в сети ${targetChainId}`);
|
||||
return {
|
||||
success: true,
|
||||
message: `Уже в сети ${networkConfig.chainName}`,
|
||||
chainId: targetChainId,
|
||||
chainName: networkConfig.chainName
|
||||
};
|
||||
}
|
||||
|
||||
// Пытаемся переключиться на существующую сеть
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_switchEthereumChain',
|
||||
params: [{ chainId: getHexChainId(targetChainId) }],
|
||||
});
|
||||
|
||||
console.log(`✅ [Network Switch] Успешно переключились на ${networkConfig.chainName}`);
|
||||
return {
|
||||
success: true,
|
||||
message: `Переключились на ${networkConfig.chainName}`,
|
||||
chainId: targetChainId,
|
||||
chainName: networkConfig.chainName
|
||||
};
|
||||
|
||||
} catch (switchError) {
|
||||
// Если сеть не добавлена в MetaMask, добавляем её
|
||||
if (switchError.code === 4902) {
|
||||
console.log(`➕ [Network Switch] Добавляем сеть ${networkConfig.chainName} в MetaMask...`);
|
||||
|
||||
try {
|
||||
await window.ethereum.request({
|
||||
method: 'wallet_addEthereumChain',
|
||||
params: [{
|
||||
chainId: getHexChainId(targetChainId),
|
||||
chainName: networkConfig.chainName,
|
||||
nativeCurrency: networkConfig.nativeCurrency,
|
||||
rpcUrls: networkConfig.rpcUrls,
|
||||
blockExplorerUrls: networkConfig.blockExplorerUrls,
|
||||
}],
|
||||
});
|
||||
|
||||
console.log(`✅ [Network Switch] Сеть ${networkConfig.chainName} добавлена и активирована`);
|
||||
return {
|
||||
success: true,
|
||||
message: `Сеть ${networkConfig.chainName} добавлена и активирована`,
|
||||
chainId: targetChainId,
|
||||
chainName: networkConfig.chainName
|
||||
};
|
||||
|
||||
} catch (addError) {
|
||||
console.error(`❌ [Network Switch] Ошибка добавления сети:`, addError);
|
||||
throw new Error(`Не удалось добавить сеть ${networkConfig.chainName}: ${addError.message}`);
|
||||
}
|
||||
} else {
|
||||
// Другие ошибки переключения
|
||||
console.error(`❌ [Network Switch] Ошибка переключения сети:`, switchError);
|
||||
throw new Error(`Не удалось переключиться на ${networkConfig.chainName}: ${switchError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [Network Switch] Ошибка:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
chainId: targetChainId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить текущую сеть
|
||||
* @returns {Promise<Object>} - Информация о текущей сети
|
||||
*/
|
||||
export async function getCurrentNetwork() {
|
||||
try {
|
||||
if (!window.ethereum) {
|
||||
throw new Error('MetaMask не найден');
|
||||
}
|
||||
|
||||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||
const numericChainId = parseInt(chainId, 16);
|
||||
const networkConfig = getNetworkConfig(numericChainId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
chainId: numericChainId,
|
||||
hexChainId: chainId,
|
||||
chainName: networkConfig?.chainName || 'Неизвестная сеть',
|
||||
isSupported: isNetworkSupported(numericChainId)
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [Network Check] Ошибка:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список поддерживаемых сетей
|
||||
* @returns {Array} - Список поддерживаемых сетей
|
||||
*/
|
||||
export function getSupportedNetworks() {
|
||||
return Object.entries(SUPPORTED_NETWORKS).map(([chainId, config]) => ({
|
||||
chainId: parseInt(chainId),
|
||||
hexChainId: getHexChainId(parseInt(chainId)),
|
||||
chainName: config.chainName,
|
||||
nativeCurrency: config.nativeCurrency,
|
||||
rpcUrls: config.rpcUrls,
|
||||
blockExplorerUrls: config.blockExplorerUrls
|
||||
}));
|
||||
}
|
||||
@@ -37,6 +37,9 @@ class WebSocketClient {
|
||||
console.log('[WebSocket] Подключение установлено');
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
|
||||
// Уведомляем о подключении
|
||||
this.emit('connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
@@ -120,6 +123,15 @@ class WebSocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Эмиссия события
|
||||
emit(event, data) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(callback => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Алиас для on() - для совместимости с useDeploymentWebSocket
|
||||
subscribe(event, callback) {
|
||||
this.on(event, callback);
|
||||
|
||||
Reference in New Issue
Block a user