ваше сообщение коммита
This commit is contained in:
@@ -57,7 +57,7 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
// Fallback к известным сетям из deploy_params или базовые
|
// Fallback к известным сетям из deploy_params или базовые
|
||||||
supportedChains = candidateChainIds.length > 0 ? candidateChainIds : [11155111, 17000, 421614, 84532];
|
supportedChains = candidateChainIds.length > 0 ? candidateChainIds : [11155111, 17000, 421614, 84532];
|
||||||
console.log(`[DLE Proposals] Используем fallback сети:`, supportedChains);
|
console.log(`[DLE Proposals] Используем fallback сети:`, supportedChains);
|
||||||
return;
|
// НЕ делаем return - продолжаем искать предложения в fallback сетях
|
||||||
}
|
}
|
||||||
if (rpcUrl) {
|
if (rpcUrl) {
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
@@ -99,6 +99,13 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
|
|
||||||
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// Проверяем, что контракт существует по этому адресу в текущей сети
|
||||||
|
const contractCode = await provider.getCode(dleAddress);
|
||||||
|
if (!contractCode || contractCode === '0x') {
|
||||||
|
console.log(`[DLE Proposals] Контракт по адресу ${dleAddress} не найден в сети ${chainId}, пропускаем`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// ABI для чтения предложений (используем getProposalSummary для мультиконтрактов)
|
// ABI для чтения предложений (используем getProposalSummary для мультиконтрактов)
|
||||||
const dleAbi = [
|
const dleAbi = [
|
||||||
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
||||||
@@ -114,13 +121,47 @@ router.post('/get-proposals', async (req, res) => {
|
|||||||
|
|
||||||
// Получаем события ProposalCreated для определения количества предложений
|
// Получаем события ProposalCreated для определения количества предложений
|
||||||
const currentBlock = await provider.getBlockNumber();
|
const currentBlock = await provider.getBlockNumber();
|
||||||
const fromBlock = Math.max(0, currentBlock - 10000); // Последние 10000 блоков
|
// RPC провайдеры ограничивают запрос до 10000 блоков, поэтому разбиваем на части
|
||||||
|
const maxBlockRange = 10000;
|
||||||
|
const searchRange = 50000; // Ищем в последних 50000 блоках
|
||||||
|
const fromBlock = Math.max(0, currentBlock - searchRange);
|
||||||
|
|
||||||
const events = await dle.queryFilter('ProposalCreated', fromBlock, currentBlock);
|
console.log(`[DLE Proposals] Проверка контракта ${dleAddress} в сети ${chainId}, диапазон блоков: ${fromBlock} - ${currentBlock}`);
|
||||||
|
|
||||||
|
// Разбиваем запрос на части по 10000 блоков
|
||||||
|
let allEvents = [];
|
||||||
|
let searchFromBlock = fromBlock;
|
||||||
|
|
||||||
|
while (searchFromBlock < currentBlock) {
|
||||||
|
const searchToBlock = Math.min(searchFromBlock + maxBlockRange - 1, currentBlock);
|
||||||
|
console.log(`[DLE Proposals] Запрос событий для блоков ${searchFromBlock} - ${searchToBlock}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chunkEvents = await dle.queryFilter('ProposalCreated', searchFromBlock, searchToBlock);
|
||||||
|
allEvents = allEvents.concat(chunkEvents);
|
||||||
|
console.log(`[DLE Proposals] Найдено событий в диапазоне ${searchFromBlock}-${searchToBlock}: ${chunkEvents.length}`);
|
||||||
|
} catch (chunkError) {
|
||||||
|
console.error(`[DLE Proposals] Ошибка при запросе блоков ${searchFromBlock}-${searchToBlock}:`, chunkError.message);
|
||||||
|
// Продолжаем с следующим диапазоном
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFromBlock = searchToBlock + 1;
|
||||||
|
|
||||||
|
// Небольшая задержка между запросами для избежания rate limiting
|
||||||
|
if (searchFromBlock < currentBlock) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = allEvents;
|
||||||
|
|
||||||
console.log(`[DLE Proposals] Найдено событий ProposalCreated в сети ${chainId}: ${events.length}`);
|
console.log(`[DLE Proposals] Найдено событий ProposalCreated в сети ${chainId}: ${events.length}`);
|
||||||
console.log(`[DLE Proposals] Диапазон блоков: ${fromBlock} - ${currentBlock}`);
|
console.log(`[DLE Proposals] Диапазон блоков: ${fromBlock} - ${currentBlock}`);
|
||||||
|
|
||||||
|
if (events.length === 0) {
|
||||||
|
console.log(`[DLE Proposals] Предложения не найдены в сети ${chainId} для контракта ${dleAddress}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Читаем информацию о каждом предложении
|
// Читаем информацию о каждом предложении
|
||||||
for (let i = 0; i < events.length; i++) {
|
for (let i = 0; i < events.length; i++) {
|
||||||
try {
|
try {
|
||||||
@@ -1303,4 +1344,133 @@ router.post('/decode-proposal-data', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Поиск предложения по transaction hash
|
||||||
|
router.post('/find-proposal-by-tx', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { transactionHash, dleAddress } = req.body;
|
||||||
|
|
||||||
|
if (!transactionHash || !dleAddress) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'transactionHash и dleAddress обязательны'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Поиск предложения по транзакции: ${transactionHash} для DLE: ${dleAddress}`);
|
||||||
|
|
||||||
|
// Получаем поддерживаемые сети DLE
|
||||||
|
let supportedChains = [];
|
||||||
|
try {
|
||||||
|
const candidateChainIds = await getSupportedChainIds();
|
||||||
|
|
||||||
|
for (const cid of candidateChainIds) {
|
||||||
|
try {
|
||||||
|
const url = await rpcProviderService.getRpcUrlByChainId(cid);
|
||||||
|
if (!url) continue;
|
||||||
|
const prov = new ethers.JsonRpcProvider(url);
|
||||||
|
const code = await prov.getCode(dleAddress);
|
||||||
|
if (code && code !== '0x') {
|
||||||
|
supportedChains.push(cid);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportedChains.length === 0) {
|
||||||
|
supportedChains = [11155111, 17000, 421614, 84532];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
supportedChains = [11155111, 17000, 421614, 84532];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем транзакцию во всех поддерживаемых сетях
|
||||||
|
for (const chainId of supportedChains) {
|
||||||
|
try {
|
||||||
|
const rpcUrl = await rpcProviderService.getRpcUrlByChainId(chainId);
|
||||||
|
if (!rpcUrl) continue;
|
||||||
|
|
||||||
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
||||||
|
|
||||||
|
// Получаем receipt транзакции
|
||||||
|
const receipt = await provider.getTransactionReceipt(transactionHash);
|
||||||
|
if (!receipt) {
|
||||||
|
console.log(`[DLE Proposals] Транзакция ${transactionHash} не найдена в сети ${chainId}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] Транзакция найдена в сети ${chainId}, блок: ${receipt.blockNumber}`);
|
||||||
|
|
||||||
|
// Ищем событие ProposalCreated в логах транзакции
|
||||||
|
const dleAbi = [
|
||||||
|
"event ProposalCreated(uint256 proposalId, address initiator, string description)"
|
||||||
|
];
|
||||||
|
const dle = new ethers.Contract(dleAddress, dleAbi, provider);
|
||||||
|
|
||||||
|
const iface = new ethers.Interface(dleAbi);
|
||||||
|
const proposalCreatedTopic = iface.getEvent('ProposalCreated').topicHash;
|
||||||
|
|
||||||
|
for (const log of receipt.logs) {
|
||||||
|
if (log.address.toLowerCase() !== dleAddress.toLowerCase()) continue;
|
||||||
|
if (log.topics[0] !== proposalCreatedTopic) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedLog = iface.parseLog(log);
|
||||||
|
const proposalId = parsedLog.args.proposalId;
|
||||||
|
|
||||||
|
console.log(`[DLE Proposals] ✅ Найдено предложение ID: ${proposalId} в сети ${chainId}`);
|
||||||
|
|
||||||
|
// Получаем полную информацию о предложении
|
||||||
|
const fullDleAbi = [
|
||||||
|
"function getProposalState(uint256 _proposalId) external view returns (uint8 state)",
|
||||||
|
"function checkProposalResult(uint256 _proposalId) external view returns (bool passed, bool quorumReached)",
|
||||||
|
"function getProposalSummary(uint256 _proposalId) external view returns (uint256 id, string memory description, uint256 forVotes, uint256 againstVotes, bool executed, bool canceled, uint256 deadline, address initiator, uint256 governanceChainId, uint256 snapshotTimepoint, uint256[] memory targetChains)"
|
||||||
|
];
|
||||||
|
const fullDle = new ethers.Contract(dleAddress, fullDleAbi, provider);
|
||||||
|
|
||||||
|
const proposalState = await fullDle.getProposalState(proposalId);
|
||||||
|
const result = await fullDle.checkProposalResult(proposalId);
|
||||||
|
const proposalData = await fullDle.getProposalSummary(proposalId);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
proposalId: Number(proposalId),
|
||||||
|
chainId: chainId,
|
||||||
|
description: parsedLog.args.description,
|
||||||
|
initiator: parsedLog.args.initiator,
|
||||||
|
transactionHash: transactionHash,
|
||||||
|
blockNumber: receipt.blockNumber,
|
||||||
|
state: Number(proposalState),
|
||||||
|
isPassed: result.passed,
|
||||||
|
quorumReached: result.quorumReached,
|
||||||
|
forVotes: Number(proposalData.forVotes),
|
||||||
|
againstVotes: Number(proposalData.againstVotes),
|
||||||
|
executed: proposalData.executed,
|
||||||
|
canceled: proposalData.canceled,
|
||||||
|
deadline: Number(proposalData.deadline)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log(`[DLE Proposals] Ошибка парсинга лога:`, parseError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[DLE Proposals] Ошибка поиска в сети ${chainId}:`, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Предложение не найдено по данной транзакции'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DLE Proposals] Ошибка при поиске предложения по транзакции:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Ошибка при поиске предложения: ' + error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -509,6 +509,7 @@ class DeployParamsService {
|
|||||||
deploymentStatus: row.deployment_status,
|
deploymentStatus: row.deployment_status,
|
||||||
deployResult: row.deploy_result,
|
deployResult: row.deploy_result,
|
||||||
deployedNetworks: deployedNetworks, // Добавляем адреса всех сетей
|
deployedNetworks: deployedNetworks, // Добавляем адреса всех сетей
|
||||||
|
networks: deployedNetworks, // Добавляем networks для фронтенда (алиас deployedNetworks)
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at
|
updatedAt: row.updated_at
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ class UnifiedDeploymentService {
|
|||||||
|
|
||||||
logger.info(`🚀 Запуск деплоя: ${scriptPath}`);
|
logger.info(`🚀 Запуск деплоя: ${scriptPath}`);
|
||||||
|
|
||||||
const hardhatPath = path.join(__dirname, '..', 'node_modules', '.bin', 'hardhat');
|
// Используем npx для более надежного запуска hardhat в Docker
|
||||||
const child = spawn(hardhatPath, ['run', scriptPath], {
|
const child = spawn('npx', ['hardhat', 'run', scriptPath], {
|
||||||
cwd: path.join(__dirname, '..'),
|
cwd: path.join(__dirname, '..'),
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
|
|||||||
661
docs/MULTICHAIN_GOVERNANCE_TOKEN_TRANSFER.md
Normal file
661
docs/MULTICHAIN_GOVERNANCE_TOKEN_TRANSFER.md
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
# Мультичейн-управление переводом токенов DLE
|
||||||
|
|
||||||
|
## Обзор системы
|
||||||
|
|
||||||
|
Система мультичейн-управления DLE позволяет холдерам токенов создавать предложения по переводу токенов со своего кошелька на другой адрес (или в казну) через процесс голосования во всех сетях, где развернут контракт DLE. Каждая сеть имеет независимый кворум, но предложения координируются и отображаются как единое целое.
|
||||||
|
|
||||||
|
## Архитектура
|
||||||
|
|
||||||
|
### Мультичейн-контракты DLE
|
||||||
|
- Один DLE может быть развернут в нескольких блокчейн-сетях (например, Sepolia, Arbitrum Sepolia, Base Sepolia)
|
||||||
|
- Каждый контракт DLE в каждой сети работает независимо
|
||||||
|
- Предложения создаются, голосуются и выполняются в каждой сети отдельно
|
||||||
|
- ID предложений уникальны для каждой сети (предложение с ID=1 в Sepolia и предложение с ID=1 в Arbitrum Sepolia - это разные предложения)
|
||||||
|
|
||||||
|
### Группировка предложений
|
||||||
|
- Предложения с одинаковым описанием и инициатором группируются в одну карточку
|
||||||
|
- Карточка отображает статус предложения во всех сетях DLE
|
||||||
|
- Каждая сеть в карточке имеет свой собственный ID предложения, состояние и результаты голосования
|
||||||
|
|
||||||
|
## Процесс перевода токенов
|
||||||
|
|
||||||
|
### Этап 1: Создание предложения
|
||||||
|
|
||||||
|
#### Описание процесса
|
||||||
|
1. Пользователь заполняет форму перевода токенов:
|
||||||
|
- **Описание предложения** - текстовое описание цели перевода
|
||||||
|
- **Адрес получателя** - адрес кошелька или казны, на который будут переведены токены
|
||||||
|
- **Количество токенов** - количество DLE токенов для перевода
|
||||||
|
- **Длительность голосования** - период времени, в течение которого можно голосовать
|
||||||
|
- **Ваш подключенный кошелек** - автоматически заполняется адресом подключенного кошелька (токены будут отправлены с этого адреса)
|
||||||
|
|
||||||
|
2. Система определяет все сети, где развернут контракт DLE
|
||||||
|
|
||||||
|
3. **Последовательное создание предложений в каждой сети:**
|
||||||
|
- Для каждой сети DLE:
|
||||||
|
- Переключение MetaMask на соответствующую сеть
|
||||||
|
- Задержка 1 секунда после переключения
|
||||||
|
- Создание предложения в контракте DLE этой сети
|
||||||
|
- Получение уникального ID предложения для этой сети
|
||||||
|
- Задержка 3 секунды после подтверждения транзакции (5 секунд для Base Sepolia)
|
||||||
|
- При ошибках RPC выполняется автоматический retry с экспоненциальной задержкой (до 3 попыток)
|
||||||
|
|
||||||
|
4. **Подписи в MetaMask:**
|
||||||
|
- Пользователь должен подписать транзакцию создания предложения в каждой сети DLE
|
||||||
|
- Количество подписей = количество сетей DLE
|
||||||
|
- Каждая подпись создает отдельное предложение в соответствующей сети
|
||||||
|
|
||||||
|
#### Технические детали
|
||||||
|
- **Функция контракта:** `createProposal(description, duration, operation, targetChains, timelockDelay)`
|
||||||
|
- **Порядок параметров:** `description`, `duration`, `operation`, `targetChains` (массив), `timelockDelay`
|
||||||
|
- **targetChains:** Массив ID сетей, где будет выполнена операция (обычно `[chainId]` для текущей сети)
|
||||||
|
- **Операция:** `_transferTokens(sender, recipient, amount)` - где `sender` = адрес инициатора предложения
|
||||||
|
- **Сигнатура:** `_transferTokens(address,address,uint256)` - **все три параметра обязательны!**
|
||||||
|
- `sender` получается автоматически из `signer.getAddress()` при создании предложения
|
||||||
|
- **ID предложения:** Генерируется автоматически контрактом в каждой сети (начинается с 0, инкрементируется)
|
||||||
|
- **Группировка:** Предложения с одинаковым `description` и `initiator` группируются в одну карточку
|
||||||
|
|
||||||
|
#### Кодирование операции
|
||||||
|
|
||||||
|
Операция для выполнения должна быть закодирована в формате ABI (Application Binary Interface) перед передачей в `createProposal`.
|
||||||
|
|
||||||
|
**Для операции перевода токенов `_transferTokens(address,address,uint256)`:**
|
||||||
|
1. **Сигнатура функции:** `_transferTokens(address,address,uint256)`
|
||||||
|
2. **Селектор функции:** Первые 4 байта от `keccak256(signature)`
|
||||||
|
3. **Параметры:**
|
||||||
|
- `sender` - адрес отправителя (инициатора предложения)
|
||||||
|
- `recipient` - адрес получателя токенов
|
||||||
|
- `amount` - количество токенов (в wei, т.е. количество * 10^18)
|
||||||
|
|
||||||
|
**Пример кодирования (JavaScript/ethers.js):**
|
||||||
|
```javascript
|
||||||
|
// Способ 1: Использование Interface (рекомендуется)
|
||||||
|
const functionSignature = '_transferTokens(address,address,uint256)';
|
||||||
|
const iface = new ethers.Interface([`function ${functionSignature}`]);
|
||||||
|
const encodedOperation = iface.encodeFunctionData('_transferTokens', [
|
||||||
|
senderAddress, // адрес инициатора
|
||||||
|
recipientAddress, // адрес получателя
|
||||||
|
ethers.parseUnits(amount.toString(), 18) // количество в wei
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Способ 2: Ручное кодирование
|
||||||
|
const functionSignature = '_transferTokens(address,address,uint256)';
|
||||||
|
const selectorBytes = ethers.keccak256(ethers.toUtf8Bytes(functionSignature));
|
||||||
|
const selector = '0x' + selectorBytes.slice(2, 10); // первые 4 байта
|
||||||
|
|
||||||
|
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
||||||
|
const encodedParams = abiCoder.encode(
|
||||||
|
['address', 'address', 'uint256'],
|
||||||
|
[senderAddress, recipientAddress, ethers.parseUnits(amount.toString(), 18)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const encodedOperation = ethers.concat([selector, encodedParams]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важные моменты:**
|
||||||
|
- `sender` должен совпадать с адресом инициатора предложения (проверяется в контракте при выполнении)
|
||||||
|
- `amount` **ОБЯЗАТЕЛЬНО** передается в wei (1 токен = 10^18 wei) - используйте `ethers.parseUnits(amount.toString(), 18)`
|
||||||
|
- **КРИТИЧЕСКИ ВАЖНО:** `sender` должен определяться из `signer.getAddress()` при создании предложения в каждой сети отдельно, а не один раз до цикла
|
||||||
|
- Операция кодируется для каждой сети отдельно с актуальным адресом signer для этой сети
|
||||||
|
- Контракт декодирует операцию при выполнении и проверяет соответствие `sender` и `initiator`
|
||||||
|
|
||||||
|
**КРИТИЧЕСКИ ВАЖНО - Правильная реализация:**
|
||||||
|
```javascript
|
||||||
|
// ✅ ПРАВИЛЬНО: Кодирование внутри цикла с актуальным адресом signer для каждой сети
|
||||||
|
async function createProposalsInAllChains(allChains, formData) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < allChains.length; index++) {
|
||||||
|
const chainId = allChains[index];
|
||||||
|
|
||||||
|
// 1. Переключаемся на нужную сеть
|
||||||
|
await switchToVotingNetwork(chainId.toString());
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Задержка после переключения
|
||||||
|
|
||||||
|
// 2. КРИТИЧЕСКИ ВАЖНО: Получаем адрес signer для текущей сети
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
const senderAddress = await signer.getAddress(); // Адрес инициатора из signer!
|
||||||
|
|
||||||
|
// 3. Кодируем операцию с актуальным адресом signer для этой сети
|
||||||
|
const transferCallData = encodeTransferTokensCall(
|
||||||
|
senderAddress, // адрес инициатора из signer (обязательно!)
|
||||||
|
formData.recipient, // адрес получателя
|
||||||
|
formData.amount // количество (будет сконвертировано в wei)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Создаем предложение
|
||||||
|
const proposalData = {
|
||||||
|
description: formData.description,
|
||||||
|
duration: formData.duration,
|
||||||
|
operation: transferCallData,
|
||||||
|
targetChains: [chainId],
|
||||||
|
timelockDelay: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
await createProposal(contractAddress, proposalData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeTransferTokensCall(sender, recipient, amount) {
|
||||||
|
const functionSignature = '_transferTokens(address,address,uint256)';
|
||||||
|
const iface = new ethers.Interface([`function ${functionSignature}`]);
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: конвертируем amount в wei
|
||||||
|
const amountInWei = ethers.parseUnits(amount.toString(), 18);
|
||||||
|
|
||||||
|
const encodedCall = iface.encodeFunctionData('_transferTokens', [
|
||||||
|
sender, // адрес инициатора (обязательно! должен быть из signer.getAddress())
|
||||||
|
recipient, // адрес получателя
|
||||||
|
amountInWei // количество в wei (обязательно!)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return encodedCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ НЕПРАВИЛЬНО: Кодирование один раз до цикла
|
||||||
|
// const transferCallData = encodeTransferTokensCall(formData.sender, ...); // НЕПРАВИЛЬНО!
|
||||||
|
// for (const chainId of allChains) {
|
||||||
|
// await createProposal(contractAddress, { operation: transferCallData, ... }); // sender может не совпасть!
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ❌ НЕПРАВИЛЬНО: Отсутствует sender или amount не в wei
|
||||||
|
// const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)"); // НЕПРАВИЛЬНО!
|
||||||
|
// const amount = transferData.amount; // НЕПРАВИЛЬНО! Нужна конвертация в wei
|
||||||
|
```
|
||||||
|
|
||||||
|
**Другие поддерживаемые операции:**
|
||||||
|
- `_addModule(bytes32,address)` - добавление модуля
|
||||||
|
- `_removeModule(bytes32)` - удаление модуля
|
||||||
|
- `_addSupportedChain(uint256)` - добавление поддерживаемой сети
|
||||||
|
- `_removeSupportedChain(uint256)` - удаление поддерживаемой сети
|
||||||
|
- `_updateVotingDurations(uint256,uint256)` - обновление времени голосования
|
||||||
|
- `_updateQuorumPercentage(uint256)` - обновление процента кворума
|
||||||
|
- `_updateDLEInfo(...)` - обновление информации DLE
|
||||||
|
- `_setLogoURI(string)` - обновление URI логотипа
|
||||||
|
|
||||||
|
#### Результат
|
||||||
|
- Создано N предложений (по одному в каждой сети DLE)
|
||||||
|
- Каждое предложение имеет уникальный ID в своей сети
|
||||||
|
- Все предложения отображаются как одна карточка в интерфейсе
|
||||||
|
- Карточка показывает статус предложения в каждой сети
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Этап 2: Голосование
|
||||||
|
|
||||||
|
#### Описание процесса
|
||||||
|
1. Пользователь видит карточку предложения с информацией о всех сетях DLE
|
||||||
|
|
||||||
|
2. Пользователь выбирает голос "За" или "Против"
|
||||||
|
|
||||||
|
3. **Последовательное голосование во всех активных сетях:**
|
||||||
|
- Система определяет все активные цепочки (state === 0 или 'active', не выполнены, не отменены)
|
||||||
|
- Для каждой активной сети:
|
||||||
|
- Переключение MetaMask на соответствующую сеть
|
||||||
|
- Задержка 1 секунда после переключения
|
||||||
|
- **Проверка баланса токенов в этой сети** (балансы могут отличаться в разных сетях)
|
||||||
|
- Если баланс отсутствует, сеть пропускается с предупреждением
|
||||||
|
- Голосование продолжается в других сетях
|
||||||
|
- Голосование с использованием **уникального ID предложения для этой сети**
|
||||||
|
- Задержка 3 секунды после подтверждения транзакции (5 секунд для Base Sepolia)
|
||||||
|
|
||||||
|
4. **Подписи в MetaMask:**
|
||||||
|
- Пользователь должен подписать транзакцию голосования в каждой активной сети DLE
|
||||||
|
- Количество подписей = количество активных сетей DLE
|
||||||
|
- Каждая подпись регистрирует голос в соответствующей сети
|
||||||
|
|
||||||
|
#### Технические детали
|
||||||
|
- **Функция контракта:** `vote(proposalId, support)` - где `proposalId` уникален для каждой сети
|
||||||
|
- **Проверка ID:** Система использует `chain.id` (ID предложения из конкретной сети), а не общий ID группы
|
||||||
|
- **Проверка баланса:** Баланс токенов проверяется **в каждой сети отдельно** перед голосованием
|
||||||
|
- В мультичейн-системе балансы могут отличаться в разных сетях
|
||||||
|
- Если в сети нет токенов, голосование в этой сети пропускается
|
||||||
|
- Контракт также проверяет баланс через `getPastVotes()` и вернет ошибку, если токенов нет
|
||||||
|
- **Вес голоса:** Зависит от баланса токенов голосующего в соответствующей сети
|
||||||
|
- **Независимые кворумы:** Каждая сеть имеет свой собственный кворум
|
||||||
|
|
||||||
|
#### Результат
|
||||||
|
- Голос зарегистрирован во всех активных сетях DLE
|
||||||
|
- Каждая сеть обновляет свои счетчики голосов (forVotes, againstVotes)
|
||||||
|
- Карточка предложения обновляется с новыми данными голосования
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Этап 3: Выполнение предложения
|
||||||
|
|
||||||
|
#### Условия выполнения
|
||||||
|
**КРИТИЧЕСКИ ВАЖНО:** Предложение может быть выполнено только при условии, что кворум достигнут **во всех сетях DLE**, где предложение активно.
|
||||||
|
|
||||||
|
Условия для каждой сети:
|
||||||
|
- Состояние предложения: `ReadyForExecution` (state === 5)
|
||||||
|
- Кворум достигнут: `forVotes >= quorumRequired`
|
||||||
|
- Большинство голосов "За": `forVotes > againstVotes`
|
||||||
|
- Предложение не выполнено: `executed === false`
|
||||||
|
- Предложение не отменено: `canceled === false`
|
||||||
|
- Истек период голосования (если применимо)
|
||||||
|
- Истек период timelock (если применимо)
|
||||||
|
|
||||||
|
#### Описание процесса
|
||||||
|
1. Система проверяет, что предложение готово к выполнению во всех активных сетях DLE
|
||||||
|
|
||||||
|
2. **Последовательное выполнение во всех готовых сетях:**
|
||||||
|
- Для каждой сети, где предложение готово к выполнению:
|
||||||
|
- Переключение MetaMask на соответствующую сеть
|
||||||
|
- Задержка 1 секунда после переключения
|
||||||
|
- Выполнение предложения с использованием **уникального ID предложения для этой сети**
|
||||||
|
- Задержка 3 секунды после подтверждения транзакции (5 секунд для Base Sepolia)
|
||||||
|
|
||||||
|
3. **Подписи в MetaMask:**
|
||||||
|
- Пользователь должен подписать транзакцию выполнения в каждой готовой сети DLE
|
||||||
|
- Количество подписей = количество готовых сетей DLE
|
||||||
|
- Каждая подпись выполняет перевод токенов в соответствующей сети
|
||||||
|
|
||||||
|
#### Технические детали
|
||||||
|
- **Функция контракта:** `executeProposal(proposalId)` - где `proposalId` уникален для каждой сети
|
||||||
|
- **Операция перевода:** `_transferTokens(sender, recipient, amount)`
|
||||||
|
- `sender` = адрес инициатора предложения (проверяется в контракте)
|
||||||
|
- `recipient` = адрес получателя из предложения
|
||||||
|
- `amount` = количество токенов из предложения
|
||||||
|
- **Проверка безопасности:** Контракт проверяет, что `sender` совпадает с `initiator` предложения
|
||||||
|
- **Перевод токенов:** Токены переводятся с кошелька инициатора, а не с баланса контракта
|
||||||
|
|
||||||
|
#### Результат
|
||||||
|
- Перевод токенов выполнен во всех готовых сетях DLE
|
||||||
|
- Каждая сеть независимо выполняет перевод с кошелька инициатора
|
||||||
|
- Карточка предложения обновляется, показывая статус "Выполнено" во всех сетях
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Отмена предложения
|
||||||
|
|
||||||
|
### Условия отмены
|
||||||
|
- Только инициатор предложения может отменить его
|
||||||
|
- Предложение должно быть активным (не выполнено, не отменено)
|
||||||
|
- Отмена возможна в любой момент до выполнения
|
||||||
|
|
||||||
|
### Процесс отмены
|
||||||
|
1. **Последовательная отмена во всех активных сетях:**
|
||||||
|
- Для каждой активной сети:
|
||||||
|
- Переключение MetaMask на соответствующую сеть
|
||||||
|
- Задержка 1 секунда после переключения
|
||||||
|
- Отмена предложения с использованием **уникального ID предложения для этой сети**
|
||||||
|
- Задержка 3 секунды после подтверждения транзакции
|
||||||
|
|
||||||
|
2. **Подписи в MetaMask:**
|
||||||
|
- Пользователь должен подписать транзакцию отмены в каждой активной сети DLE
|
||||||
|
- Количество подписей = количество активных сетей DLE
|
||||||
|
|
||||||
|
### Технические детали
|
||||||
|
- **Функция контракта:** `cancelProposal(proposalId, reason)`
|
||||||
|
- **Проверка прав:** Контракт проверяет, что вызывающий является инициатором предложения
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Важные особенности системы
|
||||||
|
|
||||||
|
### 1. Уникальность ID предложений
|
||||||
|
- **Каждая сеть имеет свой собственный счетчик предложений**
|
||||||
|
- Предложение с ID=1 в Sepolia и предложение с ID=1 в Arbitrum Sepolia - это **разные предложения**
|
||||||
|
- При группировке система сохраняет ID из каждой сети отдельно
|
||||||
|
- При голосовании/выполнении/отмене используется правильный ID для каждой сети
|
||||||
|
|
||||||
|
### 2. Группировка предложений
|
||||||
|
- Предложения группируются по ключу: `${description}_${initiator}`
|
||||||
|
- Одна карточка = одно логическое предложение во всех сетях
|
||||||
|
- Карточка отображает:
|
||||||
|
- Общее описание
|
||||||
|
- Инициатора
|
||||||
|
- Список сетей с их статусами
|
||||||
|
- Результаты голосования по каждой сети
|
||||||
|
- Общий статус (активно/выполнено/отменено)
|
||||||
|
|
||||||
|
### 3. Независимые кворумы
|
||||||
|
- **Каждая сеть имеет свой собственный кворум**
|
||||||
|
- Кворум рассчитывается на основе общего предложения токенов в соответствующей сети
|
||||||
|
- Голосование в одной сети не влияет на кворум в другой сети
|
||||||
|
- Для выполнения предложения кворум должен быть достигнут **во всех сетях**
|
||||||
|
|
||||||
|
### 4. Последовательное выполнение операций
|
||||||
|
- Все операции (создание, голосование, выполнение, отмена) выполняются **последовательно**, а не параллельно
|
||||||
|
- Это необходимо, так как MetaMask может работать только с одной сетью одновременно
|
||||||
|
- Между операциями есть задержки для стабилизации MetaMask
|
||||||
|
- **КРИТИЧЕСКИ ВАЖНО:** Использование `Promise.all` для параллельного выполнения недопустимо и приведет к ошибкам
|
||||||
|
|
||||||
|
### 5. Обработка ошибок
|
||||||
|
- **Retry для временных ошибок RPC:**
|
||||||
|
- Автоматический retry до 3 попыток
|
||||||
|
- Экспоненциальная задержка (2s, 4s, 8s)
|
||||||
|
- Только для retryable ошибок (Internal JSON-RPC error, rate limiting)
|
||||||
|
- **Ошибки в отдельных сетях:**
|
||||||
|
- Если операция не удалась в одной сети, процесс продолжается для других сетей
|
||||||
|
- Пользователь получает сводку успешных и неудачных операций
|
||||||
|
- **Обработка отсутствия баланса:**
|
||||||
|
- Перед голосованием в каждой сети проверяется баланс токенов
|
||||||
|
- Если в сети нет токенов, голосование в этой сети пропускается с предупреждением
|
||||||
|
- Голосование продолжается в других сетях, где баланс есть
|
||||||
|
- Контракт также проверяет баланс и вернет ошибку `ErrNoPower`, если токенов нет
|
||||||
|
|
||||||
|
### 6. Безопасность
|
||||||
|
- **Проверка инициатора:** При выполнении контракт проверяет, что `sender` совпадает с `initiator`
|
||||||
|
- **Проверка баланса:** Перед голосованием проверяется наличие токенов **в каждой сети отдельно**
|
||||||
|
- Балансы могут отличаться в разных сетях
|
||||||
|
- Если в сети нет токенов, голосование в этой сети пропускается
|
||||||
|
- Контракт также проверяет баланс через `getPastVotes()` и вернет ошибку `ErrNoPower`, если токенов нет
|
||||||
|
- **Проверка состояния:** Перед каждой операцией проверяется актуальное состояние предложения
|
||||||
|
- **Валидация данных:** Все данные предложения валидируются перед отправкой в контракт
|
||||||
|
|
||||||
|
### 7. Перевод токенов
|
||||||
|
- **Источник токенов:** Токены переводятся с кошелька инициатора предложения, а не с баланса контракта
|
||||||
|
- **Получатель:** Может быть любой адрес (кошелек или казна)
|
||||||
|
- **Количество:** Указывается инициатором при создании предложения
|
||||||
|
- **Проверка баланса:** Контракт проверяет достаточность баланса инициатора перед выполнением
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Пользовательский интерфейс
|
||||||
|
|
||||||
|
### Карточка предложения
|
||||||
|
- **Заголовок:** Описание предложения
|
||||||
|
- **Инициатор:** Адрес создателя предложения
|
||||||
|
- **Список сетей:**
|
||||||
|
- Название сети
|
||||||
|
- Статус (Активно/Выполнено/Отменено/Истекло)
|
||||||
|
- ID предложения в этой сети
|
||||||
|
- Результаты голосования (За/Против)
|
||||||
|
- Кворум (достигнут/не достигнут)
|
||||||
|
- **Действия:**
|
||||||
|
- Голосовать "За" / "Против" (если активно)
|
||||||
|
- Выполнить (если готово к выполнению)
|
||||||
|
- Отменить (если инициатор)
|
||||||
|
|
||||||
|
### Индикаторы статуса
|
||||||
|
- **Активно:** Предложение открыто для голосования
|
||||||
|
- **Готово к выполнению:** Кворум достигнут во всех сетях
|
||||||
|
- **Выполнено:** Перевод токенов выполнен во всех сетях
|
||||||
|
- **Отменено:** Предложение отменено инициатором
|
||||||
|
- **Истекло:** Истек период голосования
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические детали реализации
|
||||||
|
|
||||||
|
### Кодирование операций
|
||||||
|
|
||||||
|
Подробное описание процесса кодирования операций для создания предложений см. в разделе [Кодирование операции](#кодирование-операции) (Этап 1: Создание предложения).
|
||||||
|
|
||||||
|
### Структура данных предложения
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: number, // ID группы (из первой сети)
|
||||||
|
description: string, // Описание предложения
|
||||||
|
initiator: address, // Адрес инициатора
|
||||||
|
deadline: number, // Дедлайн голосования
|
||||||
|
chains: [ // Массив данных по каждой сети
|
||||||
|
{
|
||||||
|
id: number, // УНИКАЛЬНЫЙ ID для этой сети
|
||||||
|
chainId: number, // ID сети (11155111, 421614, 84532)
|
||||||
|
networkName: string, // Название сети
|
||||||
|
contractAddress: address, // Адрес контракта DLE в этой сети
|
||||||
|
state: number, // Состояние (0=Active, 3=Executed, 4=Canceled, 5=ReadyForExecution)
|
||||||
|
forVotes: bigint, // Голоса "За"
|
||||||
|
againstVotes: bigint, // Голоса "Против"
|
||||||
|
quorumRequired: bigint, // Требуемый кворум
|
||||||
|
executed: boolean, // Выполнено
|
||||||
|
canceled: boolean, // Отменено
|
||||||
|
transactionHash: string // Хеш транзакции создания
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: number, // Время создания
|
||||||
|
uniqueId: string // Уникальный ключ группировки
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Функции контракта DLE
|
||||||
|
|
||||||
|
#### createProposal
|
||||||
|
```solidity
|
||||||
|
function createProposal(
|
||||||
|
string memory _description,
|
||||||
|
uint256 _duration,
|
||||||
|
bytes memory _operation,
|
||||||
|
uint256[] memory _targetChains,
|
||||||
|
uint256 _timelockDelay
|
||||||
|
) public returns (uint256 proposalId)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### vote
|
||||||
|
```solidity
|
||||||
|
function vote(uint256 _proposalId, bool _support) public
|
||||||
|
```
|
||||||
|
|
||||||
|
#### executeProposal
|
||||||
|
```solidity
|
||||||
|
function executeProposal(uint256 _proposalId) public
|
||||||
|
```
|
||||||
|
|
||||||
|
#### cancelProposal
|
||||||
|
```solidity
|
||||||
|
function cancelProposal(uint256 _proposalId, string memory _reason) public
|
||||||
|
```
|
||||||
|
|
||||||
|
#### _transferTokens (internal)
|
||||||
|
```solidity
|
||||||
|
function _transferTokens(
|
||||||
|
address _sender,
|
||||||
|
address _recipient,
|
||||||
|
uint256 _amount
|
||||||
|
) internal
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры использования
|
||||||
|
|
||||||
|
### Пример 0: Кодирование операции перевода токенов
|
||||||
|
|
||||||
|
Перед созданием предложения необходимо закодировать операцию. **КРИТИЧЕСКИ ВАЖНО:** Используйте правильную сигнатуру и конвертируйте amount в wei.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
|
// Получаем адрес инициатора из signer
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
const sender = await signer.getAddress(); // Адрес инициатора (обязательно!)
|
||||||
|
|
||||||
|
// Параметры перевода
|
||||||
|
const recipient = '0x1234567890123456789012345678901234567890'; // Получатель
|
||||||
|
const amount = 100; // 100 токенов (в обычных единицах, не в wei)
|
||||||
|
|
||||||
|
// Кодирование операции
|
||||||
|
const functionSignature = '_transferTokens(address,address,uint256)';
|
||||||
|
const iface = new ethers.Interface([`function ${functionSignature}`]);
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: конвертируем amount в wei
|
||||||
|
const amountInWei = ethers.parseUnits(amount.toString(), 18); // 100 * 10^18 wei
|
||||||
|
|
||||||
|
const encodedOperation = iface.encodeFunctionData('_transferTokens', [
|
||||||
|
sender, // адрес инициатора (обязательно!)
|
||||||
|
recipient, // адрес получателя
|
||||||
|
amountInWei // количество в wei (обязательно!)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// encodedOperation теперь можно использовать в createProposal
|
||||||
|
// Результат: 0x... (селектор функции + закодированные параметры)
|
||||||
|
|
||||||
|
// Создание предложения с правильным порядком параметров
|
||||||
|
const tx = await dle.createProposal(
|
||||||
|
"Перевод 100 токенов в казну", // description
|
||||||
|
86400, // duration (1 день в секундах)
|
||||||
|
encodedOperation, // operation
|
||||||
|
[chainId], // targetChains (массив!)
|
||||||
|
0 // timelockDelay
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Результат:** Закодированная операция в формате bytes, готовая для передачи в `createProposal`.
|
||||||
|
|
||||||
|
**Частые ошибки:**
|
||||||
|
- ❌ Использование `_transferTokens(address,uint256)` - неправильная сигнатура
|
||||||
|
- ❌ Отсутствие параметра `sender` - контракт не сможет проверить инициатора
|
||||||
|
- ❌ Передача `amount` без конвертации в wei - неправильное количество токенов
|
||||||
|
- ❌ Неправильный порядок параметров в `createProposal` - ошибка вызова контракта
|
||||||
|
|
||||||
|
### Пример 1: Создание предложения в 3 сетях
|
||||||
|
1. Пользователь создает предложение "Перевод 100 токенов в казну"
|
||||||
|
2. Система определяет 3 сети: Sepolia, Arbitrum Sepolia, Base Sepolia
|
||||||
|
3. Создаются 3 предложения:
|
||||||
|
- Sepolia: ID=5
|
||||||
|
- Arbitrum Sepolia: ID=3
|
||||||
|
- Base Sepolia: ID=7
|
||||||
|
4. Все 3 предложения отображаются как одна карточка
|
||||||
|
|
||||||
|
### Пример 2: Голосование
|
||||||
|
1. Пользователь голосует "За" за предложение
|
||||||
|
2. Система голосует в 3 сетях:
|
||||||
|
- Sepolia: vote(5, true)
|
||||||
|
- Arbitrum Sepolia: vote(3, true)
|
||||||
|
- Base Sepolia: vote(7, true)
|
||||||
|
3. Каждое голосование требует отдельной подписи в MetaMask
|
||||||
|
|
||||||
|
### Пример 3: Выполнение
|
||||||
|
1. Кворум достигнут во всех 3 сетях
|
||||||
|
2. Система выполняет предложение в 3 сетях:
|
||||||
|
- Sepolia: executeProposal(5)
|
||||||
|
- Arbitrum Sepolia: executeProposal(3)
|
||||||
|
- Base Sepolia: executeProposal(7)
|
||||||
|
3. В каждой сети токены переводятся с кошелька инициатора на адрес получателя
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ограничения и особенности
|
||||||
|
|
||||||
|
### Ограничения
|
||||||
|
- MetaMask может работать только с одной сетью одновременно
|
||||||
|
- Операции выполняются последовательно, что может занять время при большом количестве сетей
|
||||||
|
- Ошибки RPC могут потребовать ручного retry
|
||||||
|
|
||||||
|
### Особенности
|
||||||
|
- Если предложение не создано в одной из сетей (из-за ошибки), оно все равно может быть создано в других сетях
|
||||||
|
- Голосование возможно только в тех сетях, где предложение активно
|
||||||
|
- Выполнение возможно только если кворум достигнут во всех активных сетях
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
### Защита от атак
|
||||||
|
- Проверка инициатора при выполнении
|
||||||
|
- Проверка баланса перед переводом
|
||||||
|
- Независимые кворумы в каждой сети
|
||||||
|
- Валидация всех входных данных
|
||||||
|
|
||||||
|
### Рекомендации
|
||||||
|
- Всегда проверяйте адрес получателя перед созданием предложения
|
||||||
|
- Убедитесь, что у вас достаточно токенов для перевода
|
||||||
|
- Проверяйте статус предложения перед голосованием/выполнением
|
||||||
|
- Следите за транзакциями в каждой сети
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Известные проблемы и исправления
|
||||||
|
|
||||||
|
### Исправленные критические ошибки
|
||||||
|
|
||||||
|
#### 1. Отсутствие конвертации amount в wei
|
||||||
|
**Проблема:** Параметр `amount` передавался без конвертации в wei, что приводило к неправильному количеству токенов при выполнении.
|
||||||
|
|
||||||
|
**Исправление:** Добавлена обязательная конвертация через `ethers.parseUnits(amount.toString(), 18)` перед кодированием операции.
|
||||||
|
|
||||||
|
**Файл:** `frontend/src/views/smartcontracts/TransferTokensFormView.vue`
|
||||||
|
|
||||||
|
**Статус:** ✅ Исправлено
|
||||||
|
|
||||||
|
#### 2. Неправильная сигнатура функции _transferTokens
|
||||||
|
**Проблема:** Использовалась неправильная сигнатура `_transferTokens(address,uint256)` вместо `_transferTokens(address,address,uint256)`, что приводило к ошибкам декодирования в контракте.
|
||||||
|
|
||||||
|
**Исправление:** Исправлена сигнатура функции и добавлен обязательный параметр `sender` (адрес инициатора).
|
||||||
|
|
||||||
|
**Файл:** `frontend/src/utils/dle-contract.js` (функция `createTransferTokensProposal`)
|
||||||
|
|
||||||
|
**Статус:** ✅ Исправлено
|
||||||
|
|
||||||
|
#### 3. Неправильный порядок параметров в createProposal
|
||||||
|
**Проблема:** В функцию `createProposal` передавался `governanceChainId` вместо `targetChains` в 4-м параметре, что нарушало сигнатуру контракта.
|
||||||
|
|
||||||
|
**Исправление:** Исправлен порядок параметров согласно сигнатуре контракта: `description`, `duration`, `operation`, `targetChains`, `timelockDelay`.
|
||||||
|
|
||||||
|
**Файл:** `frontend/src/utils/dle-contract.js` (функция `createTransferTokensProposal`)
|
||||||
|
|
||||||
|
**Статус:** ✅ Исправлено
|
||||||
|
|
||||||
|
#### 4. Неправильное кодирование операции при создании предложений (КРИТИЧЕСКАЯ)
|
||||||
|
**Проблема:** Операция перевода токенов кодировалась один раз до цикла по сетям, используя адрес из `formData.value.sender`. По документации, `sender` должен определяться из `signer.getAddress()` при создании предложения в каждой сети, что гарантирует совпадение с инициатором предложения.
|
||||||
|
|
||||||
|
**Исправление:**
|
||||||
|
- Кодирование операции перемещено внутрь цикла по сетям
|
||||||
|
- Добавлено получение адреса signer для каждой сети через `await signer.getAddress()`
|
||||||
|
- Добавлена проверка соответствия адреса signer адресу из формы
|
||||||
|
- Операция теперь кодируется с актуальным адресом signer для каждой сети отдельно
|
||||||
|
|
||||||
|
**Файл:** `frontend/src/views/smartcontracts/TransferTokensFormView.vue` (функция `submitForm`)
|
||||||
|
|
||||||
|
**Статус:** ✅ Исправлено (2025-01-XX)
|
||||||
|
|
||||||
|
#### 5. Параллельное выполнение в executeMultichainProposal (КРИТИЧЕСКАЯ)
|
||||||
|
**Проблема:** Функция `executeMultichainProposal` использовала `Promise.all` для параллельного выполнения операций во всех сетях одновременно. Это не работает с MetaMask, который может работать только с одной сетью одновременно.
|
||||||
|
|
||||||
|
**Исправление:**
|
||||||
|
- Заменен `Promise.all` на последовательный цикл `for`
|
||||||
|
- Добавлено переключение сетей через `switchToVotingNetwork` для каждой сети
|
||||||
|
- Добавлены задержки после переключения сетей (1 секунда) и после подтверждения транзакций (3 секунды, 5 секунд для Base Sepolia)
|
||||||
|
- Добавлена фильтрация только готовых к выполнению цепочек
|
||||||
|
|
||||||
|
**Файл:** `frontend/src/composables/useProposals.js` (функция `executeMultichainProposal`)
|
||||||
|
|
||||||
|
**Статус:** ✅ Исправлено (2025-01-XX)
|
||||||
|
|
||||||
|
#### 6. Отсутствие переключения сетей в voteOnMultichainProposal
|
||||||
|
**Проблема:** Функция `voteOnMultichainProposal` не использовала переключение сетей и проверку баланса токенов, что требовалось по документации для корректной работы в мультичейн-среде.
|
||||||
|
|
||||||
|
**Исправление:**
|
||||||
|
- Добавлено переключение сетей через `switchToVotingNetwork` для каждой сети
|
||||||
|
- Добавлена проверка баланса токенов перед голосованием в каждой сети через `checkTokenBalance`
|
||||||
|
- Добавлены задержки после переключения сетей (1 секунда) и после подтверждения транзакций (3 секунды, 5 секунд для Base Sepolia)
|
||||||
|
- Добавлена фильтрация только активных цепочек
|
||||||
|
- Добавлена обработка ошибок с пропуском сетей при отсутствии баланса
|
||||||
|
|
||||||
|
**Файл:** `frontend/src/composables/useProposals.js` (функция `voteOnMultichainProposal`)
|
||||||
|
|
||||||
|
**Статус:** ✅ Исправлено (2025-01-XX)
|
||||||
|
|
||||||
|
### Текущая корректная реализация
|
||||||
|
|
||||||
|
Все функции кодирования операций и мультичейн-операции теперь используют:
|
||||||
|
- ✅ Правильную сигнатуру: `_transferTokens(address,address,uint256)`
|
||||||
|
- ✅ Все три параметра: `sender`, `recipient`, `amount`
|
||||||
|
- ✅ Конвертацию amount в wei: `ethers.parseUnits(amount.toString(), 18)`
|
||||||
|
- ✅ Правильный порядок параметров в `createProposal`
|
||||||
|
- ✅ Определение `sender` из `signer.getAddress()` при создании предложения в каждой сети
|
||||||
|
- ✅ Последовательное выполнение операций во всех мультичейн-функциях (не параллельное)
|
||||||
|
- ✅ Переключение сетей перед каждой операцией в мультичейн-функциях
|
||||||
|
- ✅ Проверку баланса токенов перед голосованием в каждой сети
|
||||||
|
|
||||||
|
### Проверка корректности
|
||||||
|
|
||||||
|
Перед развертыванием убедитесь, что:
|
||||||
|
1. Функция `_transferTokens` кодируется с **тремя** параметрами (sender, recipient, amount)
|
||||||
|
2. `amount` всегда конвертируется в wei перед кодированием
|
||||||
|
3. `sender` получается из `signer.getAddress()` при создании предложения в каждой сети и совпадает с инициатором предложения
|
||||||
|
4. Порядок параметров в `createProposal` соответствует сигнатуре контракта
|
||||||
|
5. Все мультичейн-операции (создание, голосование, выполнение) используют последовательное выполнение с переключением сетей
|
||||||
|
6. При голосовании проверяется баланс токенов в каждой сети отдельно
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
Система мультичейн-управления DLE обеспечивает децентрализованное управление переводом токенов через независимые кворумы в каждой сети, при этом предоставляя единый интерфейс для управления предложениями во всех сетях одновременно.
|
||||||
|
|
||||||
|
**Важно:** Все критические ошибки в кодировании операций исправлены. Код соответствует документации и контракту.
|
||||||
|
|
||||||
@@ -53,29 +53,78 @@ export function useProposalValidation() {
|
|||||||
errors.push('Отсутствует описание предложения');
|
errors.push('Отсутствует описание предложения');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Для мультичейн предложений проверяем chains
|
||||||
|
// Если есть chains массив (даже с одним элементом), используем валидацию через chains
|
||||||
|
const hasChains = proposal.chains && Array.isArray(proposal.chains) && proposal.chains.length > 0;
|
||||||
|
|
||||||
|
if (hasChains) {
|
||||||
|
// Для мультичейн предложений проверяем, что есть хотя бы одна валидная цепочка
|
||||||
|
if (proposal.chains.length === 0) {
|
||||||
|
errors.push('Мультичейн предложение не содержит цепочек');
|
||||||
|
} else {
|
||||||
|
// Проверяем каждую цепочку
|
||||||
|
let validChainsCount = 0;
|
||||||
|
proposal.chains.forEach((chain, chainIndex) => {
|
||||||
|
const chainErrors = [];
|
||||||
|
|
||||||
|
if (!chain.id && chain.id !== 0) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: отсутствует ID`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chain.chainId) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: отсутствует chainId`);
|
||||||
|
} else if (!isValidChainId(chain.chainId)) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: неподдерживаемый chainId ${chain.chainId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chain.transactionHash) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: отсутствует хеш транзакции`);
|
||||||
|
} else if (!isValidTransactionHash(chain.transactionHash)) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: неверный формат хеша транзакции`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chain.state === undefined || chain.state === null) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: отсутствует статус`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof chain.forVotes !== 'number' || chain.forVotes < 0) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: неверное значение голосов "за"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof chain.againstVotes !== 'number' || chain.againstVotes < 0) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: неверное значение голосов "против"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof chain.quorumRequired !== 'number' || chain.quorumRequired < 0) {
|
||||||
|
chainErrors.push(`Цепочка ${chainIndex}: неверное значение требуемого кворума`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chainErrors.length === 0) {
|
||||||
|
validChainsCount++;
|
||||||
|
} else {
|
||||||
|
errors.push(...chainErrors);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validChainsCount === 0) {
|
||||||
|
errors.push('Мультичейн предложение не содержит валидных цепочек');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Для одиночных предложений проверяем стандартные поля
|
||||||
if (!proposal.transactionHash) {
|
if (!proposal.transactionHash) {
|
||||||
errors.push('Отсутствует хеш транзакции');
|
errors.push('Отсутствует хеш транзакции');
|
||||||
} else if (!isValidTransactionHash(proposal.transactionHash)) {
|
} else if (!isValidTransactionHash(proposal.transactionHash)) {
|
||||||
errors.push('Неверный формат хеша транзакции');
|
errors.push('Неверный формат хеша транзакции');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!proposal.initiator) {
|
|
||||||
errors.push('Отсутствует инициатор предложения');
|
|
||||||
} else if (!isValidAddress(proposal.initiator)) {
|
|
||||||
errors.push('Неверный формат адреса инициатора');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!proposal.chainId) {
|
if (!proposal.chainId) {
|
||||||
errors.push('Отсутствует chainId');
|
errors.push('Отсутствует chainId');
|
||||||
} else if (!isValidChainId(proposal.chainId)) {
|
} else if (!isValidChainId(proposal.chainId)) {
|
||||||
errors.push('Неподдерживаемый chainId');
|
errors.push('Неподдерживаемый chainId');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proposal.state === undefined || proposal.state === null) {
|
// Проверка числовых значений для одиночных предложений
|
||||||
errors.push('Отсутствует статус предложения');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверка числовых значений
|
|
||||||
if (typeof proposal.forVotes !== 'number' || proposal.forVotes < 0) {
|
if (typeof proposal.forVotes !== 'number' || proposal.forVotes < 0) {
|
||||||
errors.push('Неверное значение голосов "за"');
|
errors.push('Неверное значение голосов "за"');
|
||||||
}
|
}
|
||||||
@@ -87,6 +136,17 @@ export function useProposalValidation() {
|
|||||||
if (typeof proposal.quorumRequired !== 'number' || proposal.quorumRequired < 0) {
|
if (typeof proposal.quorumRequired !== 'number' || proposal.quorumRequired < 0) {
|
||||||
errors.push('Неверное значение требуемого кворума');
|
errors.push('Неверное значение требуемого кворума');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proposal.initiator) {
|
||||||
|
errors.push('Отсутствует инициатор предложения');
|
||||||
|
} else if (!isValidAddress(proposal.initiator)) {
|
||||||
|
errors.push('Неверный формат адреса инициатора');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposal.state === undefined || proposal.state === null) {
|
||||||
|
errors.push('Отсутствует статус предложения');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isValid: errors.length === 0,
|
isValid: errors.length === 0,
|
||||||
@@ -112,6 +172,9 @@ export function useProposalValidation() {
|
|||||||
allErrors.push({
|
allErrors.push({
|
||||||
proposalIndex: index,
|
proposalIndex: index,
|
||||||
proposalId: proposal.id,
|
proposalId: proposal.id,
|
||||||
|
description: proposal.description,
|
||||||
|
hasChains: !!(proposal.chains && Array.isArray(proposal.chains)),
|
||||||
|
chainsCount: proposal.chains?.length || 0,
|
||||||
errors: validation.errors
|
errors: validation.errors
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,6 +188,18 @@ export function useProposalValidation() {
|
|||||||
console.log(`[Proposal Validation] Валидных: ${validProposals.length}`);
|
console.log(`[Proposal Validation] Валидных: ${validProposals.length}`);
|
||||||
console.log(`[Proposal Validation] С ошибками: ${allErrors.length}`);
|
console.log(`[Proposal Validation] С ошибками: ${allErrors.length}`);
|
||||||
|
|
||||||
|
// Логируем ошибки для отладки
|
||||||
|
if (allErrors.length > 0) {
|
||||||
|
console.log(`[Proposal Validation] Детали ошибок:`, allErrors);
|
||||||
|
allErrors.forEach((error, idx) => {
|
||||||
|
console.log(`[Proposal Validation] Предложение ${idx + 1} (ID: ${error.proposalId}, описание: "${error.description || 'N/A'}"):`, {
|
||||||
|
hasChains: error.hasChains,
|
||||||
|
chainsCount: error.chainsCount,
|
||||||
|
errors: error.errors
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validProposals,
|
validProposals,
|
||||||
errors: allErrors,
|
errors: allErrors,
|
||||||
@@ -150,11 +225,16 @@ export function useProposalValidation() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Проверка, является ли предложение реальным (на основе хеша транзакции)
|
// Проверка, является ли предложение реальным (на основе хеша транзакции)
|
||||||
|
// Важно: после группировки мультичейн-предложений хеши транзакций могут жить только в proposal.chains[].transactionHash,
|
||||||
|
// поэтому проверяем и верхний уровень, и цепочки.
|
||||||
const isRealProposal = (proposal) => {
|
const isRealProposal = (proposal) => {
|
||||||
if (!proposal.transactionHash) return false;
|
const isRealTxHash = (txHash) => {
|
||||||
|
if (!txHash || typeof txHash !== 'string') return false;
|
||||||
|
|
||||||
// Проверяем, что хеш имеет правильный формат
|
// Проверяем, что хеш имеет правильный формат
|
||||||
if (!isValidTransactionHash(proposal.transactionHash)) return false;
|
if (!isValidTransactionHash(txHash)) return false;
|
||||||
|
|
||||||
|
const lower = txHash.toLowerCase();
|
||||||
|
|
||||||
// Проверяем, что это не тестовые/фейковые хеши
|
// Проверяем, что это не тестовые/фейковые хеши
|
||||||
const fakeHashes = [
|
const fakeHashes = [
|
||||||
@@ -162,14 +242,22 @@ export function useProposalValidation() {
|
|||||||
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (fakeHashes.includes(proposal.transactionHash.toLowerCase())) return false;
|
if (fakeHashes.includes(lower)) return false;
|
||||||
|
|
||||||
// Проверяем, что хеш не начинается с нулей (подозрительно)
|
|
||||||
if (proposal.transactionHash.startsWith('0x0000')) return false;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1) Одиночные предложения (или если бэкенд положил хеш на верхний уровень)
|
||||||
|
if (isRealTxHash(proposal?.transactionHash)) return true;
|
||||||
|
|
||||||
|
// 2) Сгруппированные предложения: проверяем любую цепочку
|
||||||
|
if (proposal?.chains && Array.isArray(proposal.chains) && proposal.chains.length > 0) {
|
||||||
|
return proposal.chains.some(chain => isRealTxHash(chain?.transactionHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
// Фильтрация только реальных предложений
|
// Фильтрация только реальных предложений
|
||||||
const filterRealProposals = (proposals) => {
|
const filterRealProposals = (proposals) => {
|
||||||
return proposals.filter(proposal => isRealProposal(proposal));
|
return proposals.filter(proposal => isRealProposal(proposal));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { getProposals } from '@/services/proposalsService';
|
|||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { useProposalValidation } from './useProposalValidation';
|
import { useProposalValidation } from './useProposalValidation';
|
||||||
import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract';
|
import { voteForProposal, executeProposal as executeProposalUtil, cancelProposal as cancelProposalUtil, checkTokenBalance } from '@/utils/dle-contract';
|
||||||
import axios from 'axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
|
// Функция checkVoteStatus удалена - в контракте DLE нет публичной функции hasVoted
|
||||||
// Функция checkTokenBalance перенесена в useDleContract.js
|
// Функция checkTokenBalance перенесена в useDleContract.js
|
||||||
@@ -64,7 +64,7 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
|
|
||||||
// Получаем информацию о всех DLE в разных цепочках
|
// Получаем информацию о всех DLE в разных цепочках
|
||||||
console.log('[Proposals] Получаем информацию о всех DLE...');
|
console.log('[Proposals] Получаем информацию о всех DLE...');
|
||||||
const dleResponse = await axios.get('/api/dle-v2');
|
const dleResponse = await api.get('/dle-v2');
|
||||||
|
|
||||||
if (!dleResponse.data.success) {
|
if (!dleResponse.data.success) {
|
||||||
console.error('Не удалось получить список DLE');
|
console.error('Не удалось получить список DLE');
|
||||||
@@ -81,13 +81,31 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
for (const dle of allDles) {
|
for (const dle of allDles) {
|
||||||
if (!dle.networks || dle.networks.length === 0) continue;
|
if (!dle.networks || dle.networks.length === 0) continue;
|
||||||
|
|
||||||
|
// КРИТИЧНО: Пропускаем DLE, если ни один из его адресов не совпадает с запрошенным dleAddress
|
||||||
|
const hasMatchingAddress = dle.networks.some(network =>
|
||||||
|
network.address && network.address.toLowerCase() === (dleAddress.value || '').toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dleAddress.value && !hasMatchingAddress) {
|
||||||
|
console.log(`[Proposals] Пропускаем DLE ${dle.dleAddress || 'N/A'}: адрес ${dleAddress.value} не найден в networks`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for (const network of dle.networks) {
|
for (const network of dle.networks) {
|
||||||
try {
|
try {
|
||||||
console.log(`[Proposals] Загружаем предложения из цепочки ${network.chainId}, адрес: ${network.address}`);
|
console.log(`[Proposals] Загружаем предложения из цепочки ${network.chainId}, адрес: ${network.address}`);
|
||||||
const response = await getProposals(network.address);
|
const response = await getProposals(network.address);
|
||||||
|
|
||||||
|
console.log(`[Proposals] Ответ для цепочки ${network.chainId}:`, {
|
||||||
|
success: response.success,
|
||||||
|
proposalsCount: response.data?.proposals?.length || 0,
|
||||||
|
hasError: !!response.error
|
||||||
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const chainProposals = response.data.proposals || [];
|
// Бэкенд возвращает: { success: true, data: { proposals: [...], totalCount: ... } }
|
||||||
|
const chainProposals = (response.data?.data?.proposals || response.data?.proposals || []);
|
||||||
|
console.log(`[Proposals] Получено предложений для цепочки ${network.chainId}: ${chainProposals.length}`, chainProposals);
|
||||||
|
|
||||||
// Добавляем информацию о цепочке к каждому предложению
|
// Добавляем информацию о цепочке к каждому предложению
|
||||||
chainProposals.forEach(proposal => {
|
chainProposals.forEach(proposal => {
|
||||||
@@ -95,46 +113,135 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
proposal.contractAddress = network.address;
|
proposal.contractAddress = network.address;
|
||||||
proposal.networkName = getChainName(network.chainId);
|
proposal.networkName = getChainName(network.chainId);
|
||||||
|
|
||||||
// Группируем предложения по описанию
|
// Группируем предложения по описанию и инициатору
|
||||||
const key = `${proposal.description}_${proposal.initiator}`;
|
const key = `${proposal.description}_${proposal.initiator}`;
|
||||||
|
|
||||||
|
// Преобразуем время создания в число для сравнения
|
||||||
|
// createdAt может быть ISO строкой или числом, timestamp - число в секундах
|
||||||
|
const getTimestamp = (proposal) => {
|
||||||
|
if (proposal.timestamp) return Number(proposal.timestamp); // timestamp в секундах
|
||||||
|
if (proposal.createdAt) {
|
||||||
|
if (typeof proposal.createdAt === 'string') {
|
||||||
|
return Math.floor(new Date(proposal.createdAt).getTime() / 1000); // ISO строка -> секунды
|
||||||
|
}
|
||||||
|
return Number(proposal.createdAt);
|
||||||
|
}
|
||||||
|
return Math.floor(Date.now() / 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const proposalTimestamp = getTimestamp(proposal);
|
||||||
|
|
||||||
if (!proposalsByDescription.has(key)) {
|
if (!proposalsByDescription.has(key)) {
|
||||||
proposalsByDescription.set(key, {
|
proposalsByDescription.set(key, {
|
||||||
id: proposal.id,
|
id: proposal.id, // ID из первой найденной сети
|
||||||
description: proposal.description,
|
description: proposal.description,
|
||||||
initiator: proposal.initiator,
|
initiator: proposal.initiator,
|
||||||
deadline: proposal.deadline,
|
deadline: proposal.deadline,
|
||||||
chains: new Map(),
|
chains: new Map(),
|
||||||
createdAt: Math.min(...chainProposals.map(p => p.createdAt || Date.now())),
|
createdAt: proposalTimestamp, // Время создания в секундах
|
||||||
uniqueId: key
|
uniqueId: key
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем информацию о цепочке
|
// Добавляем информацию о цепочке
|
||||||
proposalsByDescription.get(key).chains.set(network.chainId, {
|
const group = proposalsByDescription.get(key);
|
||||||
|
// Если в этой сети уже есть предложение с таким ключом, выбираем более позднее (актуальное)
|
||||||
|
const existingChainData = group.chains.get(network.chainId);
|
||||||
|
|
||||||
|
// Унифицируем state - всегда число
|
||||||
|
const normalizedState = typeof proposal.state === 'string'
|
||||||
|
? (proposal.state === 'active' ? 0 : NaN)
|
||||||
|
: Number(proposal.state);
|
||||||
|
|
||||||
|
// Убеждаемся, что id есть (fallback к proposalId из события, если id отсутствует)
|
||||||
|
const proposalId = proposal.id !== undefined && proposal.id !== null
|
||||||
|
? Number(proposal.id)
|
||||||
|
: (proposal.proposalId !== undefined ? Number(proposal.proposalId) : null);
|
||||||
|
|
||||||
|
if (existingChainData) {
|
||||||
|
// Если уже есть предложение в этой сети, сравниваем по времени создания
|
||||||
|
const existingTime = getTimestamp(existingChainData);
|
||||||
|
// Оставляем более позднее предложение (актуальное)
|
||||||
|
if (proposalTimestamp > existingTime) {
|
||||||
|
group.chains.set(network.chainId, {
|
||||||
...proposal,
|
...proposal,
|
||||||
|
id: proposalId !== null ? proposalId : existingChainData.id, // Используем id с fallback
|
||||||
chainId: network.chainId,
|
chainId: network.chainId,
|
||||||
contractAddress: network.address,
|
contractAddress: network.address,
|
||||||
networkName: getChainName(network.chainId)
|
networkName: getChainName(network.chainId),
|
||||||
|
state: normalizedState, // Унифицированный state (число)
|
||||||
|
timestamp: proposalTimestamp // Сохраняем числовой timestamp для удобства
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
// Иначе оставляем существующее
|
||||||
|
} else {
|
||||||
|
// Первое предложение в этой сети для данной группы
|
||||||
|
group.chains.set(network.chainId, {
|
||||||
|
...proposal,
|
||||||
|
id: proposalId !== null ? proposalId : 0, // Fallback к 0, если id отсутствует
|
||||||
|
chainId: network.chainId,
|
||||||
|
contractAddress: network.address,
|
||||||
|
networkName: getChainName(network.chainId),
|
||||||
|
state: normalizedState, // Унифицированный state (число)
|
||||||
|
timestamp: proposalTimestamp // Сохраняем числовой timestamp для удобства
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем createdAt группы - минимальное время из всех предложений
|
||||||
|
// После добавления нового предложения пересчитываем минимальное время
|
||||||
|
const allChainTimes = Array.from(group.chains.values())
|
||||||
|
.map(c => getTimestamp(c));
|
||||||
|
group.createdAt = Math.min(...allChainTimes, proposalTimestamp);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Ошибка загрузки предложений из цепочки ${network.chainId}:`, error);
|
console.error(`Ошибка загрузки предложений из цепочки ${network.chainId}:`, error);
|
||||||
|
console.error(`Детали ошибки для цепочки ${network.chainId}:`, {
|
||||||
|
chainId: network.chainId,
|
||||||
|
address: network.address,
|
||||||
|
errorMessage: error.message,
|
||||||
|
errorStack: error.stack
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Преобразуем в массив для отображения
|
// Преобразуем в массив для отображения
|
||||||
const rawProposals = Array.from(proposalsByDescription.values()).map(group => ({
|
const rawProposals = Array.from(proposalsByDescription.values()).map(group => {
|
||||||
|
const chainsArray = Array.from(group.chains.values()).map(chain => {
|
||||||
|
// Унифицируем state для каждого chain - всегда число
|
||||||
|
const normalizedState = typeof chain.state === 'string'
|
||||||
|
? (chain.state === 'active' ? 0 : NaN)
|
||||||
|
: Number(chain.state);
|
||||||
|
|
||||||
|
// Убеждаемся, что id есть (fallback)
|
||||||
|
const chainId = chain.id !== undefined && chain.id !== null
|
||||||
|
? Number(chain.id)
|
||||||
|
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...chain,
|
||||||
|
id: chainId !== null ? chainId : 0, // Fallback к 0, если id отсутствует
|
||||||
|
state: isNaN(normalizedState) ? 0 : normalizedState // Всегда число, fallback к 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Определяем общий state группы (число) - минимальный state из всех chains
|
||||||
|
const groupState = chainsArray.length > 0
|
||||||
|
? Math.min(...chainsArray.map(c => Number(c.state || 0)))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
...group,
|
...group,
|
||||||
chains: Array.from(group.chains.values()),
|
chains: chainsArray,
|
||||||
// Общий статус - активен если есть хотя бы одно активное предложение
|
// Общий статус - число (0 = Active, 3 = Executed, 4 = Canceled, 5 = ReadyForExecution)
|
||||||
state: group.chains.some(c => c.state === 'active') ? 'active' : 'inactive',
|
state: groupState,
|
||||||
// Общий executed - выполнен если выполнен во всех цепочках
|
// Общий executed - выполнен если выполнен во всех цепочках
|
||||||
executed: group.chains.every(c => c.executed),
|
executed: chainsArray.length > 0 && chainsArray.every(c => c.executed),
|
||||||
// Общий canceled - отменен если отменен в любой цепочке
|
// Общий canceled - отменен если отменен в любой цепочке
|
||||||
canceled: group.chains.some(c => c.canceled)
|
canceled: chainsArray.some(c => c.canceled)
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`[Proposals] Сгруппировано предложений: ${rawProposals.length}`);
|
console.log(`[Proposals] Сгруппировано предложений: ${rawProposals.length}`);
|
||||||
console.log(`[Proposals] Детали группировки:`, rawProposals);
|
console.log(`[Proposals] Детали группировки:`, rawProposals);
|
||||||
@@ -145,18 +252,20 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
// Фильтруем только реальные предложения
|
// Фильтруем только реальные предложения
|
||||||
const realProposals = filterRealProposals(validationResult.validProposals);
|
const realProposals = filterRealProposals(validationResult.validProposals);
|
||||||
|
|
||||||
// Фильтруем только активные предложения (исключаем выполненные и отмененные)
|
|
||||||
const activeProposals = filterActiveProposals(realProposals);
|
|
||||||
|
|
||||||
console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`);
|
console.log(`[Proposals] Валидных предложений: ${validationResult.validCount}`);
|
||||||
console.log(`[Proposals] Реальных предложений: ${realProposals.length}`);
|
console.log(`[Proposals] Реальных предложений: ${realProposals.length}`);
|
||||||
|
|
||||||
|
// Считаем активные только для статистики/логов (не выкидываем остальные из списка,
|
||||||
|
// иначе фильтр "Все/Выполненные/Отмененные" в UI никогда не покажет эти статусы).
|
||||||
|
const activeProposals = filterActiveProposals(realProposals);
|
||||||
console.log(`[Proposals] Активных предложений: ${activeProposals.length}`);
|
console.log(`[Proposals] Активных предложений: ${activeProposals.length}`);
|
||||||
|
|
||||||
if (validationResult.errorCount > 0) {
|
if (validationResult.errorCount > 0) {
|
||||||
console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`);
|
console.warn(`[Proposals] Найдено ${validationResult.errorCount} предложений с ошибками валидации`);
|
||||||
}
|
}
|
||||||
|
|
||||||
proposals.value = activeProposals;
|
// В UI должны попадать ВСЕ реальные предложения; дальше их фильтрует statusFilter/searchQuery
|
||||||
|
proposals.value = realProposals;
|
||||||
filterProposals();
|
filterProposals();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка загрузки предложений:', error);
|
console.error('Ошибка загрузки предложений:', error);
|
||||||
@@ -217,6 +326,12 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
throw new Error('Предложение не найдено');
|
throw new Error('Предложение не найдено');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: Если предложение мультичейн, используем voteOnMultichainProposal
|
||||||
|
if (proposal.chains && proposal.chains.length > 1) {
|
||||||
|
console.log('🌐 [VOTE] Обнаружено мультичейн предложение, используем voteOnMultichainProposal');
|
||||||
|
return await voteOnMultichainProposal(proposal, support);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📊 [DEBUG] Данные предложения:', {
|
console.log('📊 [DEBUG] Данные предложения:', {
|
||||||
id: proposal.id,
|
id: proposal.id,
|
||||||
state: proposal.state,
|
state: proposal.state,
|
||||||
@@ -339,6 +454,12 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
throw new Error('Предложение не найдено');
|
throw new Error('Предложение не найдено');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: Если предложение мультичейн, используем executeMultichainProposal
|
||||||
|
if (proposal.chains && proposal.chains.length > 1) {
|
||||||
|
console.log('🌐 [EXECUTE] Обнаружено мультичейн предложение, используем executeMultichainProposal');
|
||||||
|
return await executeMultichainProposal(proposal);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('📊 [DEBUG] Данные предложения для выполнения:', {
|
console.log('📊 [DEBUG] Данные предложения для выполнения:', {
|
||||||
id: proposal.id,
|
id: proposal.id,
|
||||||
state: proposal.state,
|
state: proposal.state,
|
||||||
@@ -417,7 +538,8 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
state: proposal.state,
|
state: proposal.state,
|
||||||
executed: proposal.executed,
|
executed: proposal.executed,
|
||||||
canceled: proposal.canceled,
|
canceled: proposal.canceled,
|
||||||
deadline: proposal.deadline
|
deadline: proposal.deadline,
|
||||||
|
chains: proposal.chains?.length || 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Проверяем, что предложение можно отменить
|
// Проверяем, что предложение можно отменить
|
||||||
@@ -429,14 +551,8 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
throw new Error('Предложение уже отменено. Повторная отмена невозможна.');
|
throw new Error('Предложение уже отменено. Повторная отмена невозможна.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что предложение активно (Pending)
|
|
||||||
if (proposal.state !== 0) {
|
|
||||||
const statusText = getProposalStatusText(proposal.state);
|
|
||||||
throw new Error(`Предложение не активно (статус: ${statusText}). Отмена возможна только для активных предложений.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, что пользователь является инициатором
|
// Проверяем, что пользователь является инициатором
|
||||||
if (proposal.initiator !== userAddress.value) {
|
if (proposal.initiator?.toLowerCase() !== userAddress.value?.toLowerCase()) {
|
||||||
throw new Error('Только инициатор предложения может его отменить.');
|
throw new Error('Только инициатор предложения может его отменить.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,16 +565,108 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отменяем предложение через готовую функцию из utils/dle-contract.js
|
// КРИТИЧЕСКИ ВАЖНО: Мультичейн отмена - последовательно во всех активных сетях
|
||||||
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
|
if (proposal.chains && proposal.chains.length > 0) {
|
||||||
|
// Фильтруем только активные цепочки (можно отменить)
|
||||||
|
const activeChains = proposal.chains.filter(chain =>
|
||||||
|
canCancel(chain) && !chain.canceled && !chain.executed
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeChains.length === 0) {
|
||||||
|
throw new Error('Не найдено ни одной активной цепочки для отмены');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 [MULTI-CANCEL] Начинаем отмену в ${activeChains.length} цепочках последовательно...`);
|
||||||
|
|
||||||
|
const { switchToVotingNetwork } = await import('@/utils/dle-contract');
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: Отменяем ПОСЛЕДОВАТЕЛЬНО, а не параллельно!
|
||||||
|
// MetaMask может работать только с одной сетью одновременно
|
||||||
|
for (let index = 0; index < activeChains.length; index++) {
|
||||||
|
const chain = activeChains[index];
|
||||||
|
console.log(`📝 [${index + 1}/${activeChains.length}] Отмена в цепочке ${chain.networkName} (${chain.chainId})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Переключаемся на нужную сеть
|
||||||
|
console.log(`🔄 [${index + 1}/${activeChains.length}] Переключаемся на сеть ${chain.chainId}...`);
|
||||||
|
const switched = await switchToVotingNetwork(chain.chainId);
|
||||||
|
if (!switched) {
|
||||||
|
throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Задержка после переключения сети
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const contractAddress = chain.contractAddress || chain.address || dleAddress.value;
|
||||||
|
// Используем ID предложения из конкретной цепочки (с fallback)
|
||||||
|
let chainProposalId = chain.id !== undefined && chain.id !== null
|
||||||
|
? Number(chain.id)
|
||||||
|
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
|
||||||
|
|
||||||
|
// Fallback к proposalId, если chain.id отсутствует
|
||||||
|
if (chainProposalId === null || isNaN(chainProposalId)) {
|
||||||
|
chainProposalId = proposalId !== undefined && proposalId !== null ? Number(proposalId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chainProposalId === null || isNaN(chainProposalId)) {
|
||||||
|
throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposalId=${proposalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
chainProposalId = Number(chainProposalId); // Убеждаемся, что это число
|
||||||
|
|
||||||
|
console.log(`🔍 [${index + 1}/${activeChains.length}] Используем ID предложения: ${chainProposalId} для отмены в цепочке ${chain.chainId}`);
|
||||||
|
|
||||||
|
// Отменяем предложение
|
||||||
|
console.log(`❌ [${index + 1}/${activeChains.length}] Отправляем отмену...`);
|
||||||
|
const result = await cancelProposalUtil(contractAddress, chainProposalId, reason);
|
||||||
|
|
||||||
|
console.log(`✅ [${index + 1}/${activeChains.length}] Предложение успешно отменено в ${chain.networkName}:`, result.txHash);
|
||||||
|
|
||||||
|
// Задержка после подтверждения транзакции (для Base Sepolia больше)
|
||||||
|
const delay = chain.chainId === 84532 ? 5000 : 3000;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: true,
|
||||||
|
txHash: result.txHash
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [${index + 1}/${activeChains.length}] Ошибка отмены в ${chain.networkName}:`, error);
|
||||||
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
// Продолжаем отменять в других цепочках даже при ошибке
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подводим итоги
|
||||||
|
const successful = results.filter(r => r.success);
|
||||||
|
const failed = results.filter(r => !r.success);
|
||||||
|
|
||||||
|
console.log(`📊 [MULTI-CANCEL] Отмена завершена: успешно в ${successful.length} из ${activeChains.length} цепочек`);
|
||||||
|
|
||||||
|
if (successful.length > 0) {
|
||||||
|
alert(`Предложение отменено в ${successful.length} из ${activeChains.length} цепочек!\n${failed.length > 0 ? `Ошибки в ${failed.length} цепочках.` : ''}`);
|
||||||
|
} else {
|
||||||
|
throw new Error('Не удалось отменить предложение ни в одной цепочке');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Одиночное предложение (без мультичейн)
|
||||||
|
const result = await cancelProposalUtil(dleAddress.value, proposalId, reason);
|
||||||
console.log('✅ Предложение успешно отменено:', result.txHash);
|
console.log('✅ Предложение успешно отменено:', result.txHash);
|
||||||
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
|
alert(`Предложение успешно отменено! Хеш транзакции: ${result.txHash}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Принудительно обновляем состояние предложения в UI
|
// Принудительно обновляем состояние предложения в UI
|
||||||
updateProposalState(proposalId, {
|
updateProposalState(proposalId, {
|
||||||
canceled: true,
|
canceled: true,
|
||||||
state: 2, // Отменено
|
state: 4, // Canceled
|
||||||
executed: false
|
executed: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -554,16 +762,32 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canVote = (proposal) => {
|
const canVote = (proposal) => {
|
||||||
return proposal.state === 0; // Pending - только активные предложения
|
// Для мультичейн предложений используем canVoteMultichain
|
||||||
|
if (proposal.chains && proposal.chains.length > 1) {
|
||||||
|
return canVoteMultichain(proposal);
|
||||||
|
}
|
||||||
|
// Унифицируем state - всегда число
|
||||||
|
const state = typeof proposal.state === 'string'
|
||||||
|
? (proposal.state === 'active' ? 0 : NaN)
|
||||||
|
: Number(proposal.state);
|
||||||
|
return state === 0; // Pending - только активные предложения
|
||||||
};
|
};
|
||||||
|
|
||||||
const canExecute = (proposal) => {
|
const canExecute = (proposal) => {
|
||||||
return proposal.state === 5; // ReadyForExecution - готово к выполнению
|
// Унифицируем state - всегда число
|
||||||
|
const state = typeof proposal.state === 'string'
|
||||||
|
? (proposal.state === 'active' ? 0 : NaN)
|
||||||
|
: Number(proposal.state);
|
||||||
|
return state === 5; // ReadyForExecution - готово к выполнению
|
||||||
};
|
};
|
||||||
|
|
||||||
const canCancel = (proposal) => {
|
const canCancel = (proposal) => {
|
||||||
|
// Унифицируем state - всегда число
|
||||||
|
const state = typeof proposal.state === 'string'
|
||||||
|
? (proposal.state === 'active' ? 0 : NaN)
|
||||||
|
: Number(proposal.state);
|
||||||
// Можно отменить только активные предложения (Pending)
|
// Можно отменить только активные предложения (Pending)
|
||||||
return proposal.state === 0 &&
|
return state === 0 &&
|
||||||
!proposal.executed &&
|
!proposal.executed &&
|
||||||
!proposal.canceled;
|
!proposal.canceled;
|
||||||
};
|
};
|
||||||
@@ -585,27 +809,110 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
try {
|
try {
|
||||||
isVoting.value = true;
|
isVoting.value = true;
|
||||||
|
|
||||||
console.log(`🌐 [MULTI-VOTE] Начинаем голосование в ${proposal.chains.length} цепочках:`, proposal.chains.map(c => c.networkName));
|
// Фильтруем только активные цепочки (state === 0 или 'active', не выполнены, не отменены)
|
||||||
|
const activeChains = proposal.chains.filter(chain => canVote(chain));
|
||||||
|
|
||||||
|
if (activeChains.length === 0) {
|
||||||
|
throw new Error('Не найдено ни одной активной цепочки для голосования');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🌐 [MULTI-VOTE] Начинаем голосование в ${activeChains.length} цепочках последовательно...`);
|
||||||
|
|
||||||
|
const { switchToVotingNetwork } = await import('@/utils/dle-contract');
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: Голосуем ПОСЛЕДОВАТЕЛЬНО, а не параллельно!
|
||||||
|
// MetaMask может работать только с одной сетью одновременно
|
||||||
|
for (let index = 0; index < activeChains.length; index++) {
|
||||||
|
const chain = activeChains[index];
|
||||||
|
console.log(`📝 [${index + 1}/${activeChains.length}] Голосование в цепочке ${chain.networkName} (${chain.chainId})`);
|
||||||
|
|
||||||
// Голосуем последовательно в каждой цепочке
|
|
||||||
for (const chain of proposal.chains) {
|
|
||||||
try {
|
try {
|
||||||
console.log(`🎯 [MULTI-VOTE] Голосуем в ${chain.networkName} (${chain.contractAddress})`);
|
// Переключаемся на нужную сеть
|
||||||
|
console.log(`🔄 [${index + 1}/${activeChains.length}] Переключаемся на сеть ${chain.chainId}...`);
|
||||||
|
const switched = await switchToVotingNetwork(chain.chainId);
|
||||||
|
if (!switched) {
|
||||||
|
throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`);
|
||||||
|
}
|
||||||
|
|
||||||
await voteForProposal(chain.contractAddress, chain.id, support);
|
// Задержка после переключения сети
|
||||||
|
|
||||||
console.log(`✅ [MULTI-VOTE] Голос отдан в ${chain.networkName}`);
|
|
||||||
|
|
||||||
// Небольшая задержка между голосованиями
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const contractAddress = chain.contractAddress || chain.address || dleAddress.value;
|
||||||
|
// Используем ID предложения из конкретной цепочки (с fallback)
|
||||||
|
let chainProposalId = chain.id !== undefined && chain.id !== null
|
||||||
|
? Number(chain.id)
|
||||||
|
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
|
||||||
|
|
||||||
|
// Fallback к proposal.id, если chain.id отсутствует
|
||||||
|
if (chainProposalId === null || isNaN(chainProposalId)) {
|
||||||
|
chainProposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chainProposalId === null || isNaN(chainProposalId)) {
|
||||||
|
throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposal.id=${proposal.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
chainProposalId = Number(chainProposalId); // Убеждаемся, что это число
|
||||||
|
|
||||||
|
console.log(`🔍 [${index + 1}/${activeChains.length}] Используем ID предложения: ${chainProposalId} для голосования в цепочке ${chain.chainId}`);
|
||||||
|
|
||||||
|
// Проверяем баланс токенов в каждой сети (балансы могут отличаться в разных сетях)
|
||||||
|
console.log(`💰 [${index + 1}/${activeChains.length}] Проверяем баланс токенов в ${chain.networkName}...`);
|
||||||
|
try {
|
||||||
|
const balanceCheck = await checkTokenBalance(contractAddress, userAddress.value);
|
||||||
|
console.log(`💰 [${index + 1}/${activeChains.length}] Баланс токенов в ${chain.networkName}:`, balanceCheck);
|
||||||
|
|
||||||
|
if (!balanceCheck.hasTokens) {
|
||||||
|
console.warn(`⚠️ [${index + 1}/${activeChains.length}] Нет токенов в ${chain.networkName}, пропускаем голосование в этой сети`);
|
||||||
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: false,
|
||||||
|
error: `Нет токенов DLE в сети ${chain.networkName} для голосования`
|
||||||
|
});
|
||||||
|
// Продолжаем с следующей сетью
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (balanceError) {
|
||||||
|
console.warn(`⚠️ [${index + 1}/${activeChains.length}] Ошибка проверки баланса в ${chain.networkName} (продолжаем):`, balanceError.message);
|
||||||
|
// При ошибке проверки баланса продолжаем попытку голосования
|
||||||
|
// Контракт сам проверит баланс и вернет ошибку, если токенов нет
|
||||||
|
}
|
||||||
|
|
||||||
|
// Голосуем
|
||||||
|
console.log(`🗳️ [${index + 1}/${activeChains.length}] Отправляем голосование для proposalId=${chainProposalId} в ${chain.networkName}...`);
|
||||||
|
const result = await voteForProposal(contractAddress, chainProposalId, support);
|
||||||
|
|
||||||
|
console.log(`✅ [${index + 1}/${activeChains.length}] Голосование успешно в ${chain.networkName}:`, result.txHash);
|
||||||
|
|
||||||
|
// Задержка после подтверждения транзакции (для Base Sepolia больше)
|
||||||
|
const delay = chain.chainId === 84532 ? 5000 : 3000;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: true,
|
||||||
|
txHash: result.txHash
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ [MULTI-VOTE] Ошибка голосования в ${chain.networkName}:`, error);
|
console.error(`❌ [${index + 1}/${activeChains.length}] Ошибка голосования в ${chain.networkName}:`, error);
|
||||||
// Продолжаем голосовать в других цепочках даже при ошибке в одной
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
// Продолжаем голосовать в других цепочках даже при ошибке
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🎉 [MULTI-VOTE] Голосование завершено во всех цепочках');
|
// Подводим итоги
|
||||||
|
const successful = results.filter(r => r.success);
|
||||||
|
const failed = results.filter(r => !r.success);
|
||||||
|
|
||||||
|
console.log(`📊 [MULTI-VOTE] Голосование завершено: успешно в ${successful.length} из ${activeChains.length} цепочек`);
|
||||||
|
|
||||||
// Перезагружаем предложения
|
// Перезагружаем предложения
|
||||||
await loadProposals();
|
await loadProposals();
|
||||||
@@ -622,26 +929,87 @@ export function useProposals(dleAddress, isAuthenticated, userAddress) {
|
|||||||
try {
|
try {
|
||||||
isExecuting.value = true;
|
isExecuting.value = true;
|
||||||
|
|
||||||
console.log(`🚀 [MULTI-EXECUTE] Начинаем исполнение в ${proposal.chains.length} цепочках`);
|
// Фильтруем только готовые к выполнению цепочки
|
||||||
|
const readyChains = proposal.chains.filter(chain => canExecute(chain));
|
||||||
|
|
||||||
// Исполняем параллельно во всех цепочках
|
if (readyChains.length === 0) {
|
||||||
const executePromises = proposal.chains.map(async (chain) => {
|
throw new Error('Нет цепочек, готовых к выполнению');
|
||||||
try {
|
|
||||||
console.log(`🎯 [MULTI-EXECUTE] Исполняем в ${chain.networkName} (${chain.contractAddress})`);
|
|
||||||
|
|
||||||
await executeProposalUtil(chain.contractAddress, chain.id);
|
|
||||||
|
|
||||||
console.log(`✅ [MULTI-EXECUTE] Исполнено в ${chain.networkName}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ [MULTI-EXECUTE] Ошибка исполнения в ${chain.networkName}:`, error);
|
|
||||||
// Продолжаем исполнение в других цепочках
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 [MULTI-EXECUTE] Начинаем исполнение в ${readyChains.length} цепочках последовательно...`);
|
||||||
|
|
||||||
|
const { switchToVotingNetwork } = await import('@/utils/dle-contract');
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: Исполняем ПОСЛЕДОВАТЕЛЬНО, а не параллельно!
|
||||||
|
// MetaMask может работать только с одной сетью одновременно
|
||||||
|
for (let index = 0; index < readyChains.length; index++) {
|
||||||
|
const chain = readyChains[index];
|
||||||
|
console.log(`📝 [${index + 1}/${readyChains.length}] Выполнение в цепочке ${chain.networkName} (${chain.chainId})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Переключаемся на нужную сеть
|
||||||
|
console.log(`🔄 [${index + 1}/${readyChains.length}] Переключаемся на сеть ${chain.chainId}...`);
|
||||||
|
const switched = await switchToVotingNetwork(chain.chainId);
|
||||||
|
if (!switched) {
|
||||||
|
throw new Error(`Не удалось переключиться на сеть ${chain.networkName} (${chain.chainId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Задержка после переключения сети
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const contractAddress = chain.contractAddress || chain.address || dleAddress.value;
|
||||||
|
// Используем ID предложения из конкретной цепочки (с fallback)
|
||||||
|
let chainProposalId = chain.id !== undefined && chain.id !== null
|
||||||
|
? Number(chain.id)
|
||||||
|
: (chain.proposalId !== undefined ? Number(chain.proposalId) : null);
|
||||||
|
|
||||||
|
// Fallback к proposal.id, если chain.id отсутствует
|
||||||
|
if (chainProposalId === null || isNaN(chainProposalId)) {
|
||||||
|
chainProposalId = proposal.id !== undefined && proposal.id !== null ? Number(proposal.id) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chainProposalId === null || isNaN(chainProposalId)) {
|
||||||
|
throw new Error(`Неверный ID предложения для цепочки ${chain.networkName} (${chain.chainId}). chain.id=${chain.id}, proposal.id=${proposal.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
chainProposalId = Number(chainProposalId); // Убеждаемся, что это число
|
||||||
|
|
||||||
|
console.log(`🔍 [${index + 1}/${readyChains.length}] Используем ID предложения: ${chainProposalId} для выполнения в цепочке ${chain.chainId}`);
|
||||||
|
|
||||||
|
// Выполняем предложение
|
||||||
|
console.log(`⚡ [${index + 1}/${readyChains.length}] Отправляем выполнение...`);
|
||||||
|
const result = await executeProposalUtil(contractAddress, chainProposalId);
|
||||||
|
|
||||||
|
console.log(`✅ [${index + 1}/${readyChains.length}] Предложение успешно выполнено в ${chain.networkName}:`, result.txHash);
|
||||||
|
|
||||||
|
// Задержка после подтверждения транзакции (для Base Sepolia больше)
|
||||||
|
const delay = chain.chainId === 84532 ? 5000 : 3000;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: true,
|
||||||
|
txHash: result.txHash
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ [${index + 1}/${readyChains.length}] Ошибка выполнения в ${chain.networkName}:`, error);
|
||||||
|
results.push({
|
||||||
|
chainId: chain.chainId,
|
||||||
|
networkName: chain.networkName,
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
// Продолжаем выполнять в других цепочках даже при ошибке
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(executePromises);
|
// Подводим итоги
|
||||||
|
const successful = results.filter(r => r.success);
|
||||||
|
const failed = results.filter(r => !r.success);
|
||||||
|
|
||||||
console.log('🎉 [MULTI-EXECUTE] Исполнение завершено во всех цепочках');
|
console.log(`📊 [MULTI-EXECUTE] Выполнение завершено: успешно в ${successful.length} из ${readyChains.length} цепочек`);
|
||||||
|
|
||||||
// Перезагружаем предложения
|
// Перезагружаем предложения
|
||||||
await loadProposals();
|
await loadProposals();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Сервис для работы с DLE v2 - основные функции
|
// Сервис для работы с DLE v2 - основные функции
|
||||||
import axios from 'axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
|
// ===== ОСНОВНЫЕ ФУНКЦИИ DLE =====
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ import axios from 'axios';
|
|||||||
*/
|
*/
|
||||||
export const getAllDLEs = async () => {
|
export const getAllDLEs = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/dle-v2');
|
const response = await api.get('/dle-v2');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении списка DLE:', error);
|
console.error('Ошибка при получении списка DLE:', error);
|
||||||
@@ -37,7 +37,7 @@ export const getAllDLEs = async () => {
|
|||||||
*/
|
*/
|
||||||
export const getDLEInfo = async (dleAddress) => {
|
export const getDLEInfo = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/dle-v2/${dleAddress}`);
|
const response = await api.get(`/dle-v2/${dleAddress}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении информации о DLE:', error);
|
console.error('Ошибка при получении информации о DLE:', error);
|
||||||
@@ -54,7 +54,7 @@ export const getDLEInfo = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const getGovernanceParams = async (dleAddress) => {
|
export const getGovernanceParams = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-core/get-governance-params', { dleAddress });
|
const response = await api.post('/dle-core/get-governance-params', { dleAddress });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении параметров управления:', error);
|
console.error('Ошибка при получении параметров управления:', error);
|
||||||
@@ -71,7 +71,7 @@ export const getGovernanceParams = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const getSupportedChains = async (dleAddress) => {
|
export const getSupportedChains = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-multichain/get-supported-chains', {
|
const response = await api.post('/dle-multichain/get-supported-chains', {
|
||||||
dleAddress
|
dleAddress
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Сервис для работы с предложениями DLE
|
// Сервис для работы с предложениями DLE
|
||||||
import axios from 'axios';
|
import api from '@/api/axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает список всех предложений
|
* Получает список всех предложений
|
||||||
@@ -21,7 +21,7 @@ import axios from 'axios';
|
|||||||
export const getProposals = async (dleAddress) => {
|
export const getProposals = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
console.log(`🌐 [API] Запрашиваем предложения для DLE: ${dleAddress}`);
|
console.log(`🌐 [API] Запрашиваем предложения для DLE: ${dleAddress}`);
|
||||||
const response = await axios.post('/dle-proposals/get-proposals', { dleAddress });
|
const response = await api.post('/dle-proposals/get-proposals', { dleAddress });
|
||||||
|
|
||||||
console.log(`🌐 [API] Ответ от backend:`, {
|
console.log(`🌐 [API] Ответ от backend:`, {
|
||||||
success: response.data.success,
|
success: response.data.success,
|
||||||
@@ -44,7 +44,7 @@ export const getProposals = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const getProposalInfo = async (dleAddress, proposalId) => {
|
export const getProposalInfo = async (dleAddress, proposalId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/get-proposal-info', {
|
const response = await api.post('/dle-proposals/get-proposal-info', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId
|
proposalId
|
||||||
});
|
});
|
||||||
@@ -63,7 +63,7 @@ export const getProposalInfo = async (dleAddress, proposalId) => {
|
|||||||
*/
|
*/
|
||||||
export const createProposal = async (dleAddress, proposalData) => {
|
export const createProposal = async (dleAddress, proposalData) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/create-proposal', {
|
const response = await api.post('/dle-proposals/create-proposal', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
...proposalData
|
...proposalData
|
||||||
});
|
});
|
||||||
@@ -92,7 +92,7 @@ export const voteOnProposal = async (dleAddress, proposalId, support, userAddres
|
|||||||
|
|
||||||
console.log('📤 [SERVICE] Отправляем запрос на голосование:', requestData);
|
console.log('📤 [SERVICE] Отправляем запрос на голосование:', requestData);
|
||||||
|
|
||||||
const response = await axios.post('/dle-proposals/vote-proposal', requestData);
|
const response = await api.post('/dle-proposals/vote-proposal', requestData);
|
||||||
|
|
||||||
console.log('📥 [SERVICE] Ответ от бэкенда:', response.data);
|
console.log('📥 [SERVICE] Ответ от бэкенда:', response.data);
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export const voteOnProposal = async (dleAddress, proposalId, support, userAddres
|
|||||||
*/
|
*/
|
||||||
export const executeProposal = async (dleAddress, proposalId) => {
|
export const executeProposal = async (dleAddress, proposalId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/execute-proposal', {
|
const response = await api.post('/dle-proposals/execute-proposal', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId
|
proposalId
|
||||||
});
|
});
|
||||||
@@ -131,7 +131,7 @@ export const executeProposal = async (dleAddress, proposalId) => {
|
|||||||
*/
|
*/
|
||||||
export const cancelProposal = async (dleAddress, proposalId, reason) => {
|
export const cancelProposal = async (dleAddress, proposalId, reason) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/cancel-proposal', {
|
const response = await api.post('/dle-proposals/cancel-proposal', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId,
|
proposalId,
|
||||||
reason
|
reason
|
||||||
@@ -151,7 +151,7 @@ export const cancelProposal = async (dleAddress, proposalId, reason) => {
|
|||||||
*/
|
*/
|
||||||
export const getProposalState = async (dleAddress, proposalId) => {
|
export const getProposalState = async (dleAddress, proposalId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/get-proposal-state', {
|
const response = await api.post('/dle-proposals/get-proposal-state', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId
|
proposalId
|
||||||
});
|
});
|
||||||
@@ -170,7 +170,7 @@ export const getProposalState = async (dleAddress, proposalId) => {
|
|||||||
*/
|
*/
|
||||||
export const getProposalVotes = async (dleAddress, proposalId) => {
|
export const getProposalVotes = async (dleAddress, proposalId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/get-proposal-votes', {
|
const response = await api.post('/dle-proposals/get-proposal-votes', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId
|
proposalId
|
||||||
});
|
});
|
||||||
@@ -189,7 +189,7 @@ export const getProposalVotes = async (dleAddress, proposalId) => {
|
|||||||
*/
|
*/
|
||||||
export const checkProposalResult = async (dleAddress, proposalId) => {
|
export const checkProposalResult = async (dleAddress, proposalId) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/check-proposal-result', {
|
const response = await api.post('/dle-proposals/check-proposal-result', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
proposalId
|
proposalId
|
||||||
});
|
});
|
||||||
@@ -207,7 +207,7 @@ export const checkProposalResult = async (dleAddress, proposalId) => {
|
|||||||
*/
|
*/
|
||||||
export const getProposalsCount = async (dleAddress) => {
|
export const getProposalsCount = async (dleAddress) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/get-proposals-count', {
|
const response = await api.post('/dle-proposals/get-proposals-count', {
|
||||||
dleAddress
|
dleAddress
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -226,7 +226,7 @@ export const getProposalsCount = async (dleAddress) => {
|
|||||||
*/
|
*/
|
||||||
export const listProposals = async (dleAddress, offset = 0, limit = 10) => {
|
export const listProposals = async (dleAddress, offset = 0, limit = 10) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/list-proposals', {
|
const response = await api.post('/dle-proposals/list-proposals', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit
|
||||||
@@ -247,7 +247,7 @@ export const listProposals = async (dleAddress, offset = 0, limit = 10) => {
|
|||||||
*/
|
*/
|
||||||
export const getVotingPowerAt = async (dleAddress, voter, timepoint) => {
|
export const getVotingPowerAt = async (dleAddress, voter, timepoint) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/get-voting-power-at', {
|
const response = await api.post('/dle-proposals/get-voting-power-at', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
voter,
|
voter,
|
||||||
timepoint
|
timepoint
|
||||||
@@ -267,7 +267,7 @@ export const getVotingPowerAt = async (dleAddress, voter, timepoint) => {
|
|||||||
*/
|
*/
|
||||||
export const getQuorumAt = async (dleAddress, timepoint) => {
|
export const getQuorumAt = async (dleAddress, timepoint) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/get-quorum-at', {
|
const response = await api.post('/dle-proposals/get-quorum-at', {
|
||||||
dleAddress,
|
dleAddress,
|
||||||
timepoint
|
timepoint
|
||||||
});
|
});
|
||||||
@@ -285,7 +285,7 @@ export const getQuorumAt = async (dleAddress, timepoint) => {
|
|||||||
*/
|
*/
|
||||||
export const decodeProposalData = async (transactionHash) => {
|
export const decodeProposalData = async (transactionHash) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/dle-proposals/decode-proposal-data', {
|
const response = await api.post('/dle-proposals/decode-proposal-data', {
|
||||||
transactionHash
|
transactionHash
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import { DLE_ABI, DLE_DEACTIVATION_ABI, TOKEN_ABI } from './dle-abi';
|
|||||||
// Функция для переключения сети кошелька
|
// Функция для переключения сети кошелька
|
||||||
export async function switchToVotingNetwork(chainId) {
|
export async function switchToVotingNetwork(chainId) {
|
||||||
try {
|
try {
|
||||||
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId}...`);
|
// Преобразуем chainId в строку для поиска в объекте networks
|
||||||
|
const chainIdStr = String(chainId);
|
||||||
|
console.log(`🔄 [NETWORK] Пытаемся переключиться на сеть ${chainId} (строка: ${chainIdStr})...`);
|
||||||
|
|
||||||
// Конфигурации сетей
|
// Конфигурации сетей
|
||||||
const networks = {
|
const networks = {
|
||||||
@@ -51,49 +53,53 @@ export async function switchToVotingNetwork(chainId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const networkConfig = networks[chainId];
|
const networkConfig = networks[chainIdStr];
|
||||||
if (!networkConfig) {
|
if (!networkConfig) {
|
||||||
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId}`);
|
console.error(`❌ [NETWORK] Неизвестная сеть: ${chainId} (строка: ${chainIdStr})`);
|
||||||
|
console.error(`❌ [NETWORK] Доступные сети:`, Object.keys(networks));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, подключена ли уже нужная сеть
|
// Проверяем, подключена ли уже нужная сеть
|
||||||
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
console.log(`🔍 [NETWORK] Текущая сеть: ${currentChainId}, нужная: ${networkConfig.chainId}`);
|
||||||
if (currentChainId === networkConfig.chainId) {
|
if (currentChainId === networkConfig.chainId) {
|
||||||
console.log(`✅ [NETWORK] Сеть ${chainId} уже подключена`);
|
console.log(`✅ [NETWORK] Сеть ${chainIdStr} уже подключена`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пытаемся переключиться на нужную сеть
|
// Пытаемся переключиться на нужную сеть
|
||||||
try {
|
try {
|
||||||
|
console.log(`🔄 [NETWORK] Запрашиваем переключение на сеть ${chainIdStr}...`);
|
||||||
await window.ethereum.request({
|
await window.ethereum.request({
|
||||||
method: 'wallet_switchEthereumChain',
|
method: 'wallet_switchEthereumChain',
|
||||||
params: [{ chainId: networkConfig.chainId }]
|
params: [{ chainId: networkConfig.chainId }]
|
||||||
});
|
});
|
||||||
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainId}`);
|
console.log(`✅ [NETWORK] Успешно переключились на сеть ${chainIdStr}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (switchError) {
|
} catch (switchError) {
|
||||||
|
console.error(`⚠️ [NETWORK] Ошибка переключения:`, switchError);
|
||||||
// Если сеть не добавлена, добавляем её
|
// Если сеть не добавлена, добавляем её
|
||||||
if (switchError.code === 4902) {
|
if (switchError.code === 4902) {
|
||||||
console.log(`➕ [NETWORK] Добавляем сеть ${chainId}...`);
|
console.log(`➕ [NETWORK] Добавляем сеть ${chainIdStr}...`);
|
||||||
try {
|
try {
|
||||||
await window.ethereum.request({
|
await window.ethereum.request({
|
||||||
method: 'wallet_addEthereumChain',
|
method: 'wallet_addEthereumChain',
|
||||||
params: [networkConfig]
|
params: [networkConfig]
|
||||||
});
|
});
|
||||||
console.log(`✅ [NETWORK] Сеть ${chainId} добавлена и подключена`);
|
console.log(`✅ [NETWORK] Сеть ${chainIdStr} добавлена и подключена`);
|
||||||
return true;
|
return true;
|
||||||
} catch (addError) {
|
} catch (addError) {
|
||||||
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainId}:`, addError);
|
console.error(`❌ [NETWORK] Ошибка добавления сети ${chainIdStr}:`, addError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainId}:`, switchError);
|
console.error(`❌ [NETWORK] Ошибка переключения на сеть ${chainIdStr}:`, switchError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ [NETWORK] Общая ошибка переключения сети:`, error);
|
console.error(`❌ [NETWORK] Общая ошибка переключения сети ${chainIdStr}:`, error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,10 +203,10 @@ export async function createProposal(dleAddress, proposalData) {
|
|||||||
const signer = await provider.getSigner();
|
const signer = await provider.getSigner();
|
||||||
|
|
||||||
// Используем общий ABI
|
// Используем общий ABI
|
||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
// Создаем предложение
|
// Создаем предложение
|
||||||
|
// Правильный порядок параметров: description, duration, operation, targetChains, timelockDelay
|
||||||
const tx = await dle.createProposal(
|
const tx = await dle.createProposal(
|
||||||
proposalData.description,
|
proposalData.description,
|
||||||
proposalData.duration,
|
proposalData.duration,
|
||||||
@@ -262,85 +268,17 @@ export async function voteForProposal(dleAddress, proposalId, support) {
|
|||||||
|
|
||||||
console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования');
|
console.log('🔍 [VOTE DEBUG] Предложение в правильном состоянии для голосования');
|
||||||
|
|
||||||
// Проверяем сеть голосования
|
// Проверяем право голоса (если доступно)
|
||||||
try {
|
try {
|
||||||
const proposal = await dle.proposals(proposalId);
|
const proposal = await dle.proposals(proposalId);
|
||||||
const currentChainId = await dle.getCurrentChainId();
|
if (proposal.snapshotTimepoint) {
|
||||||
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);
|
const votingPower = await dle.getPastVotes(signer.address, proposal.snapshotTimepoint);
|
||||||
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
|
console.log('🔍 [VOTE DEBUG] Право голоса:', votingPower.toString());
|
||||||
if (votingPower === 0n) {
|
if (votingPower === 0n) {
|
||||||
throw new Error('У пользователя нет права голоса (votingPower = 0)');
|
throw new Error('У пользователя нет права голоса (votingPower = 0)');
|
||||||
}
|
}
|
||||||
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
|
console.log('🔍 [VOTE DEBUG] У пользователя есть право голоса');
|
||||||
|
}
|
||||||
} catch (votingPowerError) {
|
} catch (votingPowerError) {
|
||||||
console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message);
|
console.warn('⚠️ [VOTE DEBUG] Не удалось проверить право голоса (продолжаем):', votingPowerError.message);
|
||||||
}
|
}
|
||||||
@@ -1089,7 +1027,6 @@ export async function loadDeactivationProposals(dleAddress) {
|
|||||||
* @param {number} transferData.amount - Количество токенов
|
* @param {number} transferData.amount - Количество токенов
|
||||||
* @param {string} transferData.description - Описание предложения
|
* @param {string} transferData.description - Описание предложения
|
||||||
* @param {number} transferData.duration - Длительность голосования в секундах
|
* @param {number} transferData.duration - Длительность голосования в секундах
|
||||||
* @param {number} transferData.governanceChainId - ID сети для голосования
|
|
||||||
* @param {Array<number>} transferData.targetChains - Целевые сети для исполнения
|
* @param {Array<number>} transferData.targetChains - Целевые сети для исполнения
|
||||||
* @returns {Promise<Object>} - Результат создания предложения
|
* @returns {Promise<Object>} - Результат создания предложения
|
||||||
*/
|
*/
|
||||||
@@ -1109,15 +1046,19 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
|||||||
|
|
||||||
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
const dle = new ethers.Contract(dleAddress, DLE_ABI, signer);
|
||||||
|
|
||||||
// Кодируем операцию перевода токенов
|
// Получаем адрес отправителя (инициатора предложения)
|
||||||
const transferFunctionSelector = ethers.id("_transferTokens(address,uint256)");
|
const senderAddress = await signer.getAddress();
|
||||||
const transferDataEncoded = ethers.AbiCoder.defaultAbiCoder().encode(
|
|
||||||
["address", "uint256"],
|
|
||||||
[transferData.recipient, ethers.parseUnits(transferData.amount.toString(), 18)]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Объединяем селектор и данные
|
// Кодируем операцию перевода токенов
|
||||||
const operation = ethers.concat([transferFunctionSelector, transferDataEncoded]);
|
// Правильная сигнатура: _transferTokens(address,address,uint256)
|
||||||
|
// Параметры: sender (инициатор), recipient (получатель), amount (в wei)
|
||||||
|
const functionSignature = '_transferTokens(address,address,uint256)';
|
||||||
|
const iface = new ethers.Interface([`function ${functionSignature}`]);
|
||||||
|
const operation = iface.encodeFunctionData('_transferTokens', [
|
||||||
|
senderAddress, // адрес инициатора
|
||||||
|
transferData.recipient, // адрес получателя
|
||||||
|
ethers.parseUnits(transferData.amount.toString(), 18) // количество в wei
|
||||||
|
]);
|
||||||
|
|
||||||
console.log('Создание предложения о переводе токенов:', {
|
console.log('Создание предложения о переводе токенов:', {
|
||||||
recipient: transferData.recipient,
|
recipient: transferData.recipient,
|
||||||
@@ -1127,11 +1068,11 @@ export async function createTransferTokensProposal(dleAddress, transferData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Создаем предложение
|
// Создаем предложение
|
||||||
|
// Правильный порядок параметров: description, duration, operation, targetChains, timelockDelay
|
||||||
const tx = await dle.createProposal(
|
const tx = await dle.createProposal(
|
||||||
transferData.description,
|
transferData.description,
|
||||||
transferData.duration,
|
transferData.duration,
|
||||||
operation,
|
operation,
|
||||||
transferData.governanceChainId,
|
|
||||||
transferData.targetChains || [],
|
transferData.targetChains || [],
|
||||||
0 // timelockDelay
|
0 // timelockDelay
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
<div class="default-logo" v-else>DLE</div>
|
<div class="default-logo" v-else>DLE</div>
|
||||||
<div class="dle-title">
|
<div class="dle-title">
|
||||||
<h3>{{ dle.name }} ({{ dle.symbol }})</h3>
|
<h3>{{ dle.name }} ({{ dle.symbol }})</h3>
|
||||||
<span class="dle-version">{{ dle.version || 'v2' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,12 +63,12 @@
|
|||||||
<li v-for="net in dle.networks" :key="net.chainId" class="network-item">
|
<li v-for="net in dle.networks" :key="net.chainId" class="network-item">
|
||||||
<span class="chain-name">{{ getChainName(net.chainId) }}:</span>
|
<span class="chain-name">{{ getChainName(net.chainId) }}:</span>
|
||||||
<a
|
<a
|
||||||
:href="getExplorerUrl(net.chainId, net.dleAddress)"
|
:href="getExplorerUrl(net.chainId, net.address)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="address-link"
|
class="address-link"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
{{ shortenAddress(net.dleAddress) }}
|
{{ shortenAddress(net.address) }}
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -574,7 +574,12 @@ const togglePreview = () => {
|
|||||||
|
|
||||||
const closeSuccessModal = () => {
|
const closeSuccessModal = () => {
|
||||||
showSuccessModal.value = false;
|
showSuccessModal.value = false;
|
||||||
goBackToProposals();
|
// Переход на страницу предложений после закрытия модалки
|
||||||
|
if (dleAddress.value) {
|
||||||
|
router.push(`/management/proposals?address=${dleAddress.value}`);
|
||||||
|
} else {
|
||||||
|
router.push('/management/proposals');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openProposals = () => {
|
const openProposals = () => {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
<div class="operations-grid">
|
<div class="operations-grid">
|
||||||
<!-- Основные операции DLE -->
|
<!-- Основные операции DLE -->
|
||||||
<div class="operation-category">
|
<div class="operation-category">
|
||||||
<h5>Основные операции DLE</h5>
|
|
||||||
<div class="operation-blocks">
|
<div class="operation-blocks">
|
||||||
<div class="operation-block">
|
<div class="operation-block">
|
||||||
<h6>Передача токенов</h6>
|
<h6>Передача токенов</h6>
|
||||||
@@ -197,7 +196,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
window.addEventListener('refresh-application-data', () => {
|
window.addEventListener('refresh-application-data', () => {
|
||||||
console.log('[CreateProposalView] Refreshing DLE proposal data');
|
console.log('[CreateProposalView] Refreshing DLE proposal data');
|
||||||
loadDLEInfo(); // Обновляем данные при входе в систему
|
loadDleData(); // Обновляем данные при входе в систему
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -106,13 +106,123 @@
|
|||||||
<span>🔗</span>
|
<span>🔗</span>
|
||||||
<span>ID: {{ proposal.uniqueId }}</span>
|
<span>ID: {{ proposal.uniqueId }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-item">
|
<!-- Мульти-чейн информация -->
|
||||||
|
<div v-if="proposal.chains && proposal.chains.length > 1" class="meta-item multichain-info">
|
||||||
|
<span>🌐</span>
|
||||||
|
<span>Цепочки ({{ proposal.chains.length }}): {{ proposal.chains.map(c => c.networkName || `Chain ${c.chainId}`).join(', ') }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="meta-item">
|
||||||
<span>⛓️</span>
|
<span>⛓️</span>
|
||||||
<span>Chain: {{ proposal.chainId }}</span>
|
<span>Chain: {{ proposal.chainId ? (proposal.chains?.[0]?.networkName || `Chain ${proposal.chainId}`) : 'N/A' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta-item">
|
<div class="meta-item">
|
||||||
<span>📄</span>
|
<span>📄</span>
|
||||||
<span>Hash: {{ (proposal.transactionHash || '').substring(0, 10) }}...</span>
|
<span>Hash: {{ ((proposal.transactionHash || proposal.chains?.[0]?.transactionHash || '')).substring(0, 10) }}...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Детали по цепочкам для мульти-чейн предложений -->
|
||||||
|
<div v-if="proposal.chains && proposal.chains.length > 1" class="chains-details">
|
||||||
|
<div class="chains-header">
|
||||||
|
<strong>Статус по цепочкам:</strong>
|
||||||
|
</div>
|
||||||
|
<div class="chains-list">
|
||||||
|
<div
|
||||||
|
v-for="chain in proposal.chains"
|
||||||
|
:key="chain.chainId"
|
||||||
|
class="chain-item"
|
||||||
|
:class="{
|
||||||
|
'chain-active': Number(chain.state) === 0,
|
||||||
|
'chain-executed': chain.executed,
|
||||||
|
'chain-canceled': chain.canceled
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="chain-main-info">
|
||||||
|
<span class="chain-name">{{ chain.networkName || `Chain ${chain.chainId}` }}</span>
|
||||||
|
<span class="chain-status">
|
||||||
|
<span v-if="chain.executed">✅ Выполнено</span>
|
||||||
|
<span v-else-if="chain.canceled">❌ Отменено</span>
|
||||||
|
<span v-else-if="chain.deadline && chain.deadline < Date.now() / 1000">⏰ Истекло</span>
|
||||||
|
<span v-else-if="Number(chain.state) === 5">🟡 Готово к выполнению</span>
|
||||||
|
<span v-else-if="Number(chain.state) === 0">🟢 Активно</span>
|
||||||
|
<span v-else>⚪ {{ chain.state }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="chain-details-info">
|
||||||
|
<div class="chain-detail-item">
|
||||||
|
<span class="detail-label">ID предложения:</span>
|
||||||
|
<span class="detail-value">#{{ chain.id !== undefined && chain.id !== null ? chain.id : 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chain-detail-item">
|
||||||
|
<span class="detail-label">Голоса:</span>
|
||||||
|
<span class="detail-value">
|
||||||
|
👍 {{ chain.forVotes ? (Number(chain.forVotes) / 1e18).toFixed(2) : '0.00' }} DLE |
|
||||||
|
👎 {{ chain.againstVotes ? (Number(chain.againstVotes) / 1e18).toFixed(2) : '0.00' }} DLE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="chain-detail-item">
|
||||||
|
<span class="detail-label">Кворум:</span>
|
||||||
|
<span class="detail-value" :class="{ 'quorum-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) >= Number(chain.quorumRequired), 'quorum-not-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) < Number(chain.quorumRequired) }">
|
||||||
|
{{ chain.forVotes && chain.quorumRequired ?
|
||||||
|
(Number(chain.forVotes) >= Number(chain.quorumRequired) ? '✅ Достигнут' : '❌ Не достигнут') :
|
||||||
|
'N/A' }}
|
||||||
|
({{ chain.quorumRequired ? (Number(chain.quorumRequired) / 1e18).toFixed(2) : '0.00' }} DLE требуется)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Для одиночных предложений тоже показываем детали -->
|
||||||
|
<div v-else-if="proposal.chains && proposal.chains.length === 1" class="chains-details">
|
||||||
|
<div class="chains-header">
|
||||||
|
<strong>Детали цепочки:</strong>
|
||||||
|
</div>
|
||||||
|
<div class="chains-list">
|
||||||
|
<div
|
||||||
|
v-for="chain in proposal.chains"
|
||||||
|
:key="chain.chainId"
|
||||||
|
class="chain-item"
|
||||||
|
:class="{
|
||||||
|
'chain-active': Number(chain.state) === 0,
|
||||||
|
'chain-executed': chain.executed,
|
||||||
|
'chain-canceled': chain.canceled
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="chain-main-info">
|
||||||
|
<span class="chain-name">{{ chain.networkName || `Chain ${chain.chainId}` }}</span>
|
||||||
|
<span class="chain-status">
|
||||||
|
<span v-if="chain.executed">✅ Выполнено</span>
|
||||||
|
<span v-else-if="chain.canceled">❌ Отменено</span>
|
||||||
|
<span v-else-if="chain.state === 5">🟡 Готово к выполнению</span>
|
||||||
|
<span v-else-if="Number(chain.state) === 0">🟢 Активно</span>
|
||||||
|
<span v-else>⚪ {{ chain.state }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="chain-details-info">
|
||||||
|
<div class="chain-detail-item">
|
||||||
|
<span class="detail-label">ID предложения:</span>
|
||||||
|
<span class="detail-value">#{{ chain.id !== undefined && chain.id !== null ? chain.id : proposal.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="chain-detail-item">
|
||||||
|
<span class="detail-label">Голоса:</span>
|
||||||
|
<span class="detail-value">
|
||||||
|
👍 {{ chain.forVotes ? (Number(chain.forVotes) / 1e18).toFixed(2) : '0.00' }} DLE |
|
||||||
|
👎 {{ chain.againstVotes ? (Number(chain.againstVotes) / 1e18).toFixed(2) : '0.00' }} DLE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="chain-detail-item">
|
||||||
|
<span class="detail-label">Кворум:</span>
|
||||||
|
<span class="detail-value" :class="{ 'quorum-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) >= Number(chain.quorumRequired), 'quorum-not-reached': chain.forVotes && chain.quorumRequired && Number(chain.forVotes) < Number(chain.quorumRequired) }">
|
||||||
|
{{ chain.forVotes && chain.quorumRequired ?
|
||||||
|
(Number(chain.forVotes) >= Number(chain.quorumRequired) ? '✅ Достигнут' : '❌ Не достигнут') :
|
||||||
|
'N/A' }}
|
||||||
|
({{ chain.quorumRequired ? (Number(chain.quorumRequired) / 1e18).toFixed(2) : '0.00' }} DLE требуется)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -132,7 +242,7 @@
|
|||||||
|
|
||||||
<div class="proposal-actions">
|
<div class="proposal-actions">
|
||||||
<button
|
<button
|
||||||
v-if="canVote(proposal)"
|
v-if="proposal.chains && proposal.chains.length > 1 ? canVoteMultichain(proposal) : canVote(proposal)"
|
||||||
@click="voteOnProposal(proposal.id, true)"
|
@click="voteOnProposal(proposal.id, true)"
|
||||||
class="btn btn-success"
|
class="btn btn-success"
|
||||||
:disabled="isVoting"
|
:disabled="isVoting"
|
||||||
@@ -140,7 +250,7 @@
|
|||||||
{{ isVoting ? 'Голосование...' : 'За' }}
|
{{ isVoting ? 'Голосование...' : 'За' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canVote(proposal)"
|
v-if="proposal.chains && proposal.chains.length > 1 ? canVoteMultichain(proposal) : canVote(proposal)"
|
||||||
@click="voteOnProposal(proposal.id, false)"
|
@click="voteOnProposal(proposal.id, false)"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
:disabled="isVoting"
|
:disabled="isVoting"
|
||||||
@@ -148,7 +258,7 @@
|
|||||||
{{ isVoting ? 'Голосование...' : 'Против' }}
|
{{ isVoting ? 'Голосование...' : 'Против' }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canExecute(proposal)"
|
v-if="proposal.chains && proposal.chains.length > 1 ? canExecuteMultichain(proposal) : canExecute(proposal)"
|
||||||
@click="executeProposal(proposal.id)"
|
@click="executeProposal(proposal.id)"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
:disabled="isExecuting"
|
:disabled="isExecuting"
|
||||||
@@ -174,7 +284,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { useAuth } from '@/composables/useAuth';
|
import { useAuthContext } from '@/composables/useAuth';
|
||||||
import { useProposals } from '@/composables/useProposals';
|
import { useProposals } from '@/composables/useProposals';
|
||||||
import BaseLayout from '@/components/BaseLayout.vue';
|
import BaseLayout from '@/components/BaseLayout.vue';
|
||||||
|
|
||||||
@@ -205,7 +315,7 @@ export default {
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { currentAddress, address } = useAuth();
|
const { address } = useAuthContext();
|
||||||
|
|
||||||
const dleAddress = computed(() => {
|
const dleAddress = computed(() => {
|
||||||
return route.query.address;
|
return route.query.address;
|
||||||
@@ -230,7 +340,9 @@ export default {
|
|||||||
getQuorumPercentage,
|
getQuorumPercentage,
|
||||||
getRequiredQuorumPercentage,
|
getRequiredQuorumPercentage,
|
||||||
canVote,
|
canVote,
|
||||||
|
canVoteMultichain,
|
||||||
canExecute,
|
canExecute,
|
||||||
|
canExecuteMultichain,
|
||||||
canCancel
|
canCancel
|
||||||
} = useProposals(dleAddress, computed(() => props.isAuthenticated), address);
|
} = useProposals(dleAddress, computed(() => props.isAuthenticated), address);
|
||||||
|
|
||||||
@@ -278,7 +390,9 @@ export default {
|
|||||||
getQuorumPercentage,
|
getQuorumPercentage,
|
||||||
getRequiredQuorumPercentage,
|
getRequiredQuorumPercentage,
|
||||||
canVote,
|
canVote,
|
||||||
|
canVoteMultichain,
|
||||||
canExecute,
|
canExecute,
|
||||||
|
canExecuteMultichain,
|
||||||
canCancel
|
canCancel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -601,6 +715,117 @@ export default {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multichain-info {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chains-details {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chains-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chains-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-item.chain-active {
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-item.chain-executed {
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-item.chain-canceled {
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-main-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-status {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-details-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain-detail-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: #333;
|
||||||
|
text-align: right;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value.quorum-reached {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value.quorum-not-reached {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.proposals-page {
|
.proposals-page {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import BaseLayout from '../../components/BaseLayout.vue';
|
import BaseLayout from '../../components/BaseLayout.vue';
|
||||||
import api from '@/api/axios';
|
import api from '@/api/axios';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { createProposal } from '@/utils/dle-contract';
|
import { createProposal, switchToVotingNetwork } from '@/utils/dle-contract';
|
||||||
import { useAuthContext } from '../../composables/useAuth';
|
import { useAuthContext } from '../../composables/useAuth';
|
||||||
|
|
||||||
// Определяем props
|
// Определяем props
|
||||||
@@ -264,14 +264,35 @@ async function loadDleInfo() {
|
|||||||
try {
|
try {
|
||||||
isLoadingDle.value = true;
|
isLoadingDle.value = true;
|
||||||
|
|
||||||
// Получаем информацию о DLE из блокчейна
|
// Получаем информацию о DLE из API, который возвращает все развернутые сети
|
||||||
const response = await api.post('/blockchain/read-dle-info', {
|
const response = await api.get('/dle-v2');
|
||||||
dleAddress: dleAddress.value
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
dleInfo.value = response.data.data;
|
const allDles = response.data.data || [];
|
||||||
|
console.log('All DLEs from API:', allDles);
|
||||||
|
|
||||||
|
// Ищем DLE по адресу (может быть в любой из сетей)
|
||||||
|
let foundDle = null;
|
||||||
|
for (const dle of allDles) {
|
||||||
|
// Проверяем, есть ли этот адрес в deployedNetworks
|
||||||
|
const networkMatch = dle.deployedNetworks?.find(net =>
|
||||||
|
net.address?.toLowerCase() === dleAddress.value.toLowerCase()
|
||||||
|
);
|
||||||
|
if (networkMatch) {
|
||||||
|
foundDle = dle;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundDle) {
|
||||||
|
// Используем deployedNetworks из найденного DLE
|
||||||
|
dleInfo.value = {
|
||||||
|
...foundDle,
|
||||||
|
deployedNetworks: foundDle.deployedNetworks || []
|
||||||
|
};
|
||||||
console.log('DLE Info loaded:', dleInfo.value);
|
console.log('DLE Info loaded:', dleInfo.value);
|
||||||
|
console.log('Deployed networks count:', dleInfo.value?.deployedNetworks?.length || 0);
|
||||||
|
console.log('Deployed networks:', dleInfo.value?.deployedNetworks);
|
||||||
|
|
||||||
// Получаем поддерживаемые цепочки из данных DLE
|
// Получаем поддерживаемые цепочки из данных DLE
|
||||||
if (dleInfo.value.deployedNetworks && dleInfo.value.deployedNetworks.length > 0) {
|
if (dleInfo.value.deployedNetworks && dleInfo.value.deployedNetworks.length > 0) {
|
||||||
@@ -283,6 +304,18 @@ async function loadDleInfo() {
|
|||||||
console.warn('No deployed networks found for DLE');
|
console.warn('No deployed networks found for DLE');
|
||||||
supportedChains.value = [];
|
supportedChains.value = [];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('DLE not found in API response, trying blockchain read...');
|
||||||
|
// Fallback: получаем информацию из блокчейна (только текущая сеть)
|
||||||
|
const blockchainResponse = await api.post('/blockchain/read-dle-info', {
|
||||||
|
dleAddress: dleAddress.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blockchainResponse.data.success) {
|
||||||
|
dleInfo.value = blockchainResponse.data.data;
|
||||||
|
console.log('DLE Info loaded from blockchain:', dleInfo.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -334,17 +367,85 @@ function getChainName(chainId) {
|
|||||||
return chainNames[chainId] || `Chain ${chainId}`;
|
return chainNames[chainId] || `Chain ${chainId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создание encoded call data для _transferTokens
|
// Функция для проверки, является ли ошибка временной RPC ошибкой
|
||||||
function encodeTransferTokensCall(sender, recipient, amount) {
|
function isRetryableRpcError(error) {
|
||||||
// Правильный селектор для _transferTokens(address,address,uint256)
|
if (!error) return false;
|
||||||
// keccak256("_transferTokens(address,address,uint256)")[:4]
|
|
||||||
const functionSignature = '_transferTokens(address,address,uint256)';
|
|
||||||
const selectorBytes = ethers.keccak256(ethers.toUtf8Bytes(functionSignature));
|
|
||||||
const selector = '0x' + selectorBytes.slice(2, 10);
|
|
||||||
|
|
||||||
// Кодирование параметров
|
const errorMessage = error.message?.toLowerCase() || '';
|
||||||
|
const errorCode = error.code;
|
||||||
|
|
||||||
|
// Проверяем на временные RPC ошибки
|
||||||
|
const retryablePatterns = [
|
||||||
|
'internal json-rpc error',
|
||||||
|
'json-rpc error',
|
||||||
|
'rpc error',
|
||||||
|
'network error',
|
||||||
|
'timeout',
|
||||||
|
'connection',
|
||||||
|
'econnrefused',
|
||||||
|
'etimedout',
|
||||||
|
'could not coalesce error',
|
||||||
|
'rate limit',
|
||||||
|
'too many requests'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Коды ошибок, которые можно повторить
|
||||||
|
const retryableCodes = [-32603, -32000, -32002, -32005];
|
||||||
|
|
||||||
|
return retryablePatterns.some(pattern => errorMessage.includes(pattern)) ||
|
||||||
|
retryableCodes.includes(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция retry с экспоненциальной задержкой
|
||||||
|
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
|
||||||
|
// Если это не временная RPC ошибка, не повторяем
|
||||||
|
if (!isRetryableRpcError(error)) {
|
||||||
|
console.log(`❌ [RETRY] Не повторяемая ошибка:`, error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это последняя попытка, выбрасываем ошибку
|
||||||
|
if (attempt === maxRetries) {
|
||||||
|
console.log(`❌ [RETRY] Исчерпаны все попытки (${maxRetries})`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем задержку с экспоненциальным backoff
|
||||||
|
const delay = initialDelay * Math.pow(2, attempt - 1);
|
||||||
|
console.log(`🔄 [RETRY] Попытка ${attempt}/${maxRetries} не удалась, повтор через ${delay}ms...`);
|
||||||
|
console.log(`🔄 [RETRY] Ошибка:`, error.message);
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создание encoded call data для _transferTokens
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: используйте правильную сигнатуру _transferTokens(address,address,uint256)
|
||||||
|
// и конвертируйте amount в wei
|
||||||
|
function encodeTransferTokensCall(sender, recipient, amount) {
|
||||||
|
const functionSignature = '_transferTokens(address,address,uint256)';
|
||||||
const iface = new ethers.Interface([`function ${functionSignature}`]);
|
const iface = new ethers.Interface([`function ${functionSignature}`]);
|
||||||
const encodedCall = iface.encodeFunctionData('_transferTokens', [sender, recipient, amount]);
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: конвертируем amount в wei (1 токен = 10^18 wei)
|
||||||
|
const amountInWei = ethers.parseUnits(amount.toString(), 18);
|
||||||
|
|
||||||
|
// Кодирование операции с тремя параметрами: sender, recipient, amountInWei
|
||||||
|
const encodedCall = iface.encodeFunctionData('_transferTokens', [
|
||||||
|
sender, // адрес инициатора (обязательно!)
|
||||||
|
recipient, // адрес получателя
|
||||||
|
amountInWei // количество в wei (обязательно!)
|
||||||
|
]);
|
||||||
|
|
||||||
return encodedCall;
|
return encodedCall;
|
||||||
}
|
}
|
||||||
@@ -360,8 +461,8 @@ async function submitForm() {
|
|||||||
throw new Error('Некорректный адрес отправителя');
|
throw new Error('Некорректный адрес отправителя');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, что адрес отправителя совпадает с адресом пользователя
|
// Проверяем, что адрес отправителя совпадает с адресом пользователя (case-insensitive)
|
||||||
if (formData.value.sender !== currentUserAddress.value) {
|
if (formData.value.sender.toLowerCase() !== currentUserAddress.value?.toLowerCase()) {
|
||||||
throw new Error('Адрес отправителя должен совпадать с вашим подключенным кошельком');
|
throw new Error('Адрес отправителя должен совпадать с вашим подключенным кошельком');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +470,16 @@ async function submitForm() {
|
|||||||
throw new Error('Некорректный адрес получателя');
|
throw new Error('Некорректный адрес получателя');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем, что получатель не является zero address
|
||||||
|
if (formData.value.recipient.toLowerCase() === '0x0000000000000000000000000000000000000000') {
|
||||||
|
throw new Error('Адрес получателя не может быть нулевым адресом');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что отправитель и получатель не совпадают
|
||||||
|
if (formData.value.sender.toLowerCase() === formData.value.recipient.toLowerCase()) {
|
||||||
|
throw new Error('Адрес отправителя и получателя не могут совпадать');
|
||||||
|
}
|
||||||
|
|
||||||
if (!formData.value.amount || formData.value.amount <= 0) {
|
if (!formData.value.amount || formData.value.amount <= 0) {
|
||||||
throw new Error('Некорректное количество токенов');
|
throw new Error('Некорректное количество токенов');
|
||||||
}
|
}
|
||||||
@@ -381,23 +492,73 @@ async function submitForm() {
|
|||||||
throw new Error('Выберите время голосования');
|
throw new Error('Выберите время голосования');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создание encoded call data для передачи токенов
|
// Получаем все поддерживаемые цепочки из DLE информации
|
||||||
|
console.log('DLE Info for proposal creation:', dleInfo.value);
|
||||||
|
console.log('Deployed networks:', dleInfo.value?.deployedNetworks);
|
||||||
|
|
||||||
|
if (!dleInfo.value?.deployedNetworks || dleInfo.value.deployedNetworks.length === 0) {
|
||||||
|
throw new Error('Не найдены развернутые сети для DLE контракта');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allChains = dleInfo.value.deployedNetworks.map(net => {
|
||||||
|
console.log('Network info:', { chainId: net.chainId, address: net.address, name: net.networkName });
|
||||||
|
return net.chainId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Creating proposals in chains:', allChains);
|
||||||
|
console.log('Number of chains:', allChains.length);
|
||||||
|
|
||||||
|
if (allChains.length === 0) {
|
||||||
|
throw new Error('Не найдено ни одной цепочки для создания предложений');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем предложения последовательно во всех цепочках с переключением сети
|
||||||
|
console.log(`🚀 Starting to create ${allChains.length} proposals sequentially...`);
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < allChains.length; index++) {
|
||||||
|
const chainId = allChains[index];
|
||||||
|
console.log(`📝 [${index + 1}/${allChains.length}] Starting proposal creation for chain ${chainId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Переключаемся на нужную сеть перед созданием предложения
|
||||||
|
console.log(`🔄 [${index + 1}/${allChains.length}] Switching to network ${chainId}...`);
|
||||||
|
const networkSwitched = await switchToVotingNetwork(chainId);
|
||||||
|
console.log(`🔄 [${index + 1}/${allChains.length}] Network switch result:`, networkSwitched);
|
||||||
|
|
||||||
|
if (!networkSwitched) {
|
||||||
|
throw new Error(`Не удалось переключиться на сеть ${chainId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем текущую сеть после переключения
|
||||||
|
const currentChainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||||||
|
console.log(`🔍 [${index + 1}/${allChains.length}] Current chain after switch:`, currentChainId, `Expected: 0x${chainId.toString(16)}`);
|
||||||
|
|
||||||
|
// Небольшая задержка после переключения сети
|
||||||
|
console.log(`⏳ [${index + 1}/${allChains.length}] Waiting 1 second after network switch...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// КРИТИЧЕСКИ ВАЖНО: Получаем адрес signer для текущей сети
|
||||||
|
// Это гарантирует, что sender в операции совпадает с инициатором предложения
|
||||||
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
|
const signer = await provider.getSigner();
|
||||||
|
const senderAddress = await signer.getAddress();
|
||||||
|
console.log(`🔑 [${index + 1}/${allChains.length}] Sender address for chain ${chainId}:`, senderAddress);
|
||||||
|
|
||||||
|
// Проверяем, что адрес signer совпадает с адресом из формы
|
||||||
|
if (senderAddress.toLowerCase() !== formData.value.sender.toLowerCase()) {
|
||||||
|
throw new Error(`Адрес signer (${senderAddress}) не совпадает с адресом отправителя из формы (${formData.value.sender})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кодируем операцию перевода токенов для текущей сети
|
||||||
|
// Используем адрес signer, чтобы гарантировать совпадение с инициатором предложения
|
||||||
const transferCallData = encodeTransferTokensCall(
|
const transferCallData = encodeTransferTokensCall(
|
||||||
formData.value.sender,
|
senderAddress,
|
||||||
formData.value.recipient,
|
formData.value.recipient,
|
||||||
formData.value.amount
|
formData.value.amount
|
||||||
);
|
);
|
||||||
|
|
||||||
// Получаем все поддерживаемые цепочки из DLE информации
|
|
||||||
const allChains = dleInfo.value?.deployedNetworks
|
|
||||||
? dleInfo.value.deployedNetworks.map(net => net.chainId)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
console.log('Creating proposals in chains:', allChains);
|
|
||||||
|
|
||||||
// Создаем предложения параллельно во всех цепочках
|
|
||||||
const proposalPromises = allChains.map(async (chainId) => {
|
|
||||||
try {
|
|
||||||
const proposalData = {
|
const proposalData = {
|
||||||
description: formData.value.description,
|
description: formData.value.description,
|
||||||
duration: parseInt(formData.value.votingDuration),
|
duration: parseInt(formData.value.votingDuration),
|
||||||
@@ -406,38 +567,68 @@ async function submitForm() {
|
|||||||
timelockDelay: 0
|
timelockDelay: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Creating proposal in chain ${chainId}:`, proposalData);
|
console.log(`📋 [${index + 1}/${allChains.length}] Proposal data for chain ${chainId}:`, proposalData);
|
||||||
|
|
||||||
// Получаем адрес контракта для этой цепочки
|
// Получаем адрес контракта для этой цепочки
|
||||||
const networkInfo = dleInfo.value?.deployedNetworks?.find(net => net.chainId === chainId);
|
const networkInfo = dleInfo.value?.deployedNetworks?.find(net => net.chainId === chainId);
|
||||||
const contractAddress = networkInfo?.address || dleAddress.value;
|
const contractAddress = networkInfo?.address || dleAddress.value;
|
||||||
|
|
||||||
const result = await createProposal(contractAddress, proposalData);
|
console.log(`🔄 [${index + 1}/${allChains.length}] Calling createProposal for chain ${chainId}, contract: ${contractAddress}`);
|
||||||
|
|
||||||
return {
|
// Используем retry для временных RPC ошибок
|
||||||
|
const result = await retryWithBackoff(
|
||||||
|
async () => {
|
||||||
|
return await createProposal(contractAddress, proposalData);
|
||||||
|
},
|
||||||
|
3, // Максимум 3 попытки
|
||||||
|
2000 // Начальная задержка 2 секунды
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ [${index + 1}/${allChains.length}] Proposal created successfully in chain ${chainId}:`, result);
|
||||||
|
|
||||||
|
// Дополнительная задержка после подтверждения транзакции
|
||||||
|
// чтобы MetaMask успел обработать транзакцию перед переходом к следующей цепочке
|
||||||
|
// Для Base Sepolia увеличиваем задержку, так как уведомления могут приходить медленнее
|
||||||
|
if (result.success && result.txHash) {
|
||||||
|
const delay = chainId === 84532 ? 5000 : 3000; // 5 секунд для Base Sepolia, 3 для остальных
|
||||||
|
console.log(`⏳ [${index + 1}/${allChains.length}] Waiting ${delay/1000} seconds for MetaMask to process transaction in ${getChainName(chainId)}...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({
|
||||||
chainId,
|
chainId,
|
||||||
success: result.success,
|
success: result.success,
|
||||||
proposalId: result.proposalId,
|
proposalId: result.proposalId,
|
||||||
|
txHash: result.txHash,
|
||||||
error: result.error,
|
error: result.error,
|
||||||
contractAddress
|
contractAddress
|
||||||
};
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error creating proposal in chain ${chainId}:`, error);
|
console.error(`❌ [${index + 1}/${allChains.length}] Error creating proposal in chain ${chainId}:`, error);
|
||||||
return {
|
console.error(`❌ [${index + 1}/${allChains.length}] Error details:`, {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
name: error.name
|
||||||
|
});
|
||||||
|
results.push({
|
||||||
chainId,
|
chainId,
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message,
|
error: error.message || 'Неизвестная ошибка',
|
||||||
contractAddress: dleAddress.value
|
contractAddress: dleInfo.value?.deployedNetworks?.find(net => net.chainId === chainId)?.address || dleAddress.value
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const results = await Promise.all(proposalPromises);
|
console.log(`📊 Всего обработано цепочек: ${results.length} из ${allChains.length}`);
|
||||||
|
console.log(`📊 Результаты создания предложений:`, results);
|
||||||
|
|
||||||
// Проверяем результаты
|
// Проверяем результаты
|
||||||
const successful = results.filter(r => r.success);
|
const successful = results.filter(r => r.success);
|
||||||
const failed = results.filter(r => !r.success);
|
const failed = results.filter(r => !r.success);
|
||||||
|
|
||||||
|
console.log(`✅ Успешно создано в ${successful.length} цепочках`);
|
||||||
|
console.log(`❌ Ошибок в ${failed.length} цепочках`);
|
||||||
|
|
||||||
if (successful.length > 0) {
|
if (successful.length > 0) {
|
||||||
proposalResult.value = {
|
proposalResult.value = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -447,6 +638,10 @@ async function submitForm() {
|
|||||||
failedChains: failed
|
failedChains: failed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Автоматический переход на страницу предложений
|
||||||
|
console.log('🔄 Переход на страницу предложений...');
|
||||||
|
router.push(`/management/proposals?address=${dleAddress.value}`);
|
||||||
|
|
||||||
// Очистка формы только при полном успехе
|
// Очистка формы только при полном успехе
|
||||||
if (failed.length === 0) {
|
if (failed.length === 0) {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
|
|||||||
@@ -131,19 +131,40 @@ fi
|
|||||||
echo -e "${YELLOW}📦 Синхронизация docker-compose.prod.yml...${NC}"
|
echo -e "${YELLOW}📦 Синхронизация docker-compose.prod.yml...${NC}"
|
||||||
scp -e "ssh $SSH_OPTS" ./webssh-agent/docker-compose.prod.yml "$VDS_USER@$VDS_HOST:$VDS_PATH/docker-compose.prod.yml"
|
scp -e "ssh $SSH_OPTS" ./webssh-agent/docker-compose.prod.yml "$VDS_USER@$VDS_HOST:$VDS_PATH/docker-compose.prod.yml"
|
||||||
|
|
||||||
|
# Синхронизация Dockerfile файлов (если они изменились)
|
||||||
|
echo -e "${YELLOW}📦 Синхронизация Dockerfile файлов...${NC}"
|
||||||
|
scp -e "ssh $SSH_OPTS" ./backend/Dockerfile "$VDS_USER@$VDS_HOST:$VDS_PATH/backend/Dockerfile" 2>/dev/null || true
|
||||||
|
scp -e "ssh $SSH_OPTS" ./frontend/Dockerfile "$VDS_USER@$VDS_HOST:$VDS_PATH/frontend/Dockerfile" 2>/dev/null || true
|
||||||
|
scp -e "ssh $SSH_OPTS" ./frontend/nginx.Dockerfile "$VDS_USER@$VDS_HOST:$VDS_PATH/frontend/nginx.Dockerfile" 2>/dev/null || true
|
||||||
|
|
||||||
echo -e "${GREEN}✅ Синхронизация завершена!${NC}"
|
echo -e "${GREEN}✅ Синхронизация завершена!${NC}"
|
||||||
|
|
||||||
# Спрашиваем, нужно ли пересобрать образы
|
# Спрашиваем, нужно ли пересобрать образы
|
||||||
read -p "Пересобрать Docker образы на VDS? (y/n): " -n 1 -r
|
read -p "Пересобрать Docker образы на VDS? (y/n): " -n 1 -r
|
||||||
echo
|
echo
|
||||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
echo -e "${YELLOW}🔨 Пересборка Docker образов на VDS...${NC}"
|
echo -e "${YELLOW}🔨 Пересборка Docker образов на VDS (без кеша)...${NC}"
|
||||||
ssh $SSH_OPTS $VDS_USER@$VDS_HOST "cd $VDS_PATH && docker compose -f docker-compose.prod.yml build backend frontend frontend-nginx && docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
echo -e "${YELLOW}⏳ Это может занять несколько минут...${NC}"
|
||||||
|
|
||||||
|
# Пересобираем образы БЕЗ кеша для гарантированного применения изменений
|
||||||
|
ssh $SSH_OPTS $VDS_USER@$VDS_HOST "cd $VDS_PATH && \
|
||||||
|
docker compose -f docker-compose.prod.yml build --no-cache backend frontend frontend-nginx && \
|
||||||
|
docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
echo -e "${GREEN}✅ Образы пересобраны и контейнеры перезапущены!${NC}"
|
echo -e "${GREEN}✅ Образы пересобраны и контейнеры перезапущены!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Ошибка при пересборке образов!${NC}"
|
||||||
|
echo -e "${YELLOW}💡 Попробуйте пересобрать вручную:${NC}"
|
||||||
|
echo -e " ssh -p $VDS_PORT $VDS_USER@$VDS_HOST"
|
||||||
|
echo -e " cd $VDS_PATH"
|
||||||
|
echo -e " docker compose -f docker-compose.prod.yml build --no-cache backend frontend frontend-nginx"
|
||||||
|
echo -e " docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo -e "${YELLOW}💡 Для пересборки образов выполните:${NC}"
|
echo -e "${YELLOW}💡 Для пересборки образов выполните:${NC}"
|
||||||
echo -e " ssh -p $VDS_PORT $VDS_USER@$VDS_HOST"
|
echo -e " ssh -p $VDS_PORT $VDS_USER@$VDS_HOST"
|
||||||
echo -e " cd $VDS_PATH"
|
echo -e " cd $VDS_PATH"
|
||||||
echo -e " docker compose -f docker-compose.prod.yml build backend frontend frontend-nginx"
|
echo -e " docker compose -f docker-compose.prod.yml build --no-cache backend frontend frontend-nginx"
|
||||||
echo -e " docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
echo -e " docker compose -f docker-compose.prod.yml up -d --force-recreate backend frontend frontend-nginx"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user