Files
DLE/frontend/src/views/settings/BlockchainSettingsView.vue

1290 lines
54 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="blockchain-settings settings-panel">
<!-- Панель Создать новое DLE (Digital Legal Entity) -->
<div class="sub-settings-panel">
<h3>Создать новое DLE (Digital Legal Entity)</h3>
<div class="setting-form">
<p>Настройка и деплой нового DLE (Digital Legal Entity) с токеном управления и контрактом Governor.</p>
<!-- 1. Имя DLE -->
<div class="form-group">
<label class="form-label" for="dleName">Имя DLE (Digital Legal Entity) (и токена):</label>
<input type="text" id="dleName" v-model="dleDeploymentSettings.name" class="form-control" placeholder="Например, My DLE">
</div>
<!-- 2. Символ токена -->
<div class="form-group">
<label class="form-label" for="dleSymbol">Символ токена управления (GT):</label>
<input type="text" id="dleSymbol" v-model="dleDeploymentSettings.symbol" class="form-control" placeholder="Например, MDGT (3-5 символов)">
</div>
<!-- 3. Местонахождение -->
<h4>Местонахождение</h4>
<div class="address-grid">
<div class="form-group address-index">
<label class="form-label" for="locIndex">Индекс:</label>
<div class="input-group">
<input type="text" id="locIndex" v-model="dleDeploymentSettings.locationIndex" @input="checkIndexInput" class="form-control">
<button class="btn btn-outline-secondary btn-sm" type="button" @click="fetchAddressByZipcode" :disabled="isFetchingByZipcode || !dleDeploymentSettings.locationIndex">
<i class="fas fa-search"></i> {{ isFetchingByZipcode ? 'Поиск...' : 'Найти по индексу' }}
</button>
</div>
</div>
<template v-if="addressFieldsVisible">
<div class="form-group address-country">
<label class="form-label" for="locCountry">Страна:</label>
<input type="text" id="locCountry" v-model="dleDeploymentSettings.locationCountry" class="form-control">
</div>
<div class="form-group address-city">
<label class="form-label" for="locCity">Населенный пункт:</label>
<input type="text" id="locCity" v-model="dleDeploymentSettings.locationCity" class="form-control">
</div>
<div class="form-group address-street">
<label class="form-label" for="locStreet">Улица:</label>
<input type="text" id="locStreet" v-model="dleDeploymentSettings.locationStreet" class="form-control">
</div>
<div class="form-group address-house">
<label class="form-label" for="locHouse">Дом:</label>
<input type="text" id="locHouse" v-model="dleDeploymentSettings.locationHouse" class="form-control">
</div>
<div class="form-group address-office">
<label class="form-label" for="locOffice">Офис/Кв.:</label>
<input type="text" id="locOffice" v-model="dleDeploymentSettings.locationOffice" class="form-control">
</div>
</template>
</div>
<div class="address-verification-section" v-if="addressFieldsVisible">
<button @click="verifyAddress" :disabled="isAddressVerifying" class="btn btn-info btn-sm">
<i class="fas fa-search-location"></i> {{ isAddressVerifying ? 'Проверка адреса...' : 'Проверить адрес' }}
</button>
<div v-if="addressVerificationResult" class="verification-status alert mt-2">
<p v-if="addressVerificationResult === 'verified_exact'" class="alert-success"> Адрес подтвержден (точный).</p>
<p v-if="addressVerificationResult === 'verified_street'" class="alert-success"> Адрес подтвержден (улица найдена).</p>
<p v-if="addressVerificationResult === 'verified_city'" class="alert-success"> Адрес подтвержден (город найден).</p>
<p v-if="addressVerificationResult === 'verified_ambiguous'" class="alert-warning"> Адрес найден, но требует уточнения.</p>
<p v-if="addressVerificationResult === 'not_found'" class="alert-danger"> Адрес не найден.</p>
<p v-if="addressVerificationResult === 'ambiguous'" class="alert-warning"> Найденный адрес не полностью совпадает с введенным. Уточните запрос.</p>
<p v-if="addressVerificationResult === 'error'" class="alert-danger"> Ошибка при проверке адреса.</p>
<details v-if="verifiedAddressDetails" class="mt-2">
<summary>Детали от Nominatim</summary>
<pre class="code-block">{{ JSON.stringify(verifiedAddressDetails, null, 2) }}</pre>
</details>
</div>
</div>
<!-- 4. Код деятельности -->
<h4>Код деятельности</h4>
<div v-if="dleDeploymentSettings.selectedIsicCodes && dleDeploymentSettings.selectedIsicCodes.length > 0" class="isic-codes-list mt-3">
<h5>Добавленные коды деятельности:</h5>
<ul>
<li v-for="(isic, index) in dleDeploymentSettings.selectedIsicCodes" :key="index" class="d-flex justify-content-between align-items-center mb-1">
<span>{{ isic.text }} ({{ isic.code }})</span>
<button @click="removeIsicCode(index)" class="btn btn-danger btn-xs">Удалить</button>
</li>
</ul>
</div>
<div class="form-group">
<label class="form-label" for="isicSection">Выберите код деятельности:</label>
<select id="isicSection" v-model="selectedSection" class="form-control" :disabled="isLoadingSections">
<option value="">-- {{ isLoadingSections ? 'Загрузка секций...' : 'Выберите секцию' }} --</option>
<option v-for="option in sectionOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
<div class="form-group" v-if="selectedSection">
<label class="form-label" for="isicDivision">Раздел:</label>
<select id="isicDivision" v-model="selectedDivision" class="form-control" :disabled="isLoadingDivisions">
<option value="">-- {{ isLoadingDivisions ? 'Загрузка разделов...' : 'Выберите раздел' }} --</option>
<option v-for="option in divisionOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
<div class="form-group" v-if="selectedDivision">
<label class="form-label" for="isicGroup">Группа:</label>
<select id="isicGroup" v-model="selectedGroup" class="form-control" :disabled="isLoadingGroups">
<option value="">-- {{ isLoadingGroups ? 'Загрузка групп...' : 'Выберите группу' }} --</option>
<option v-for="option in groupOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
<div class="form-group" v-if="selectedGroup">
<label class="form-label" for="isicClass">Класс:</label>
<select id="isicClass" v-model="selectedClass" class="form-control" :disabled="isLoadingClasses">
<option value="">-- {{ isLoadingClasses ? 'Загрузка классов...' : 'Выберите класс' }} --</option>
<option v-for="option in classOptions" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
</div>
<div v-if="currentSelectedIsicText" class="current-isic-selection">
<p><strong>Выбранный код:</strong> {{ currentSelectedIsicText }}</p>
<button @click="addIsicCode" class="btn btn-success btn-sm" :disabled="!currentSelectedIsicCode">Добавить код деятельности</button>
</div>
<!-- 5. Первоначальное распределение токенов -->
<h4>Первоначальное распределение токенов управления</h4>
<div v-for="(partner, index) in dleDeploymentSettings.partners" :key="index" class="partner-entry">
<div class="form-group">
<label class="form-label">Адрес партнера {{ index + 1 }}:</label>
<input type="text" v-model="partner.address" class="form-control" placeholder="0x...">
</div>
<div class="form-group">
<label class="form-label">Сумма GT для партнера {{ index + 1 }}:</label>
<input type="number" v-model="partner.amount" min="1" class="form-control">
</div>
<button class="btn btn-danger btn-sm" @click="removePartner(index)">Удалить партнера</button>
</div>
<button class="btn btn-secondary" @click="addPartner">Добавить партнера</button>
<div class="form-group">
<label class="form-label">Общее количество выпускаемых GT: {{ totalInitialSupply }}</label>
</div>
<!-- 6. Настройки Governor -->
<h4>Настройки Governor</h4>
<div class="form-group">
<label class="form-label" for="proposalThreshold">Порог для создания предложений (кол-во GT):</label>
<input type="number" id="proposalThreshold" v-model="dleDeploymentSettings.proposalThreshold" min="0" class="form-control">
</div>
<div class="form-group">
<label class="form-label" for="quorumPercentGovernor">Кворум (% от общего числа голосов):</label>
<input type="number" id="quorumPercentGovernor" v-model="dleDeploymentSettings.quorumPercent" min="1" max="100" class="form-control">
</div>
<div class="form-group">
<label class="form-label" for="votingDelay">Задержка перед голосованием (в днях):</label>
<input type="number" id="votingDelay" v-model="dleDeploymentSettings.votingDelayDays" min="0" class="form-control">
</div>
<div class="form-group">
<label class="form-label" for="votingPeriod">Период голосования (в днях):</label>
<input type="number" id="votingPeriod" v-model="dleDeploymentSettings.votingPeriodDays" min="1" class="form-control">
</div>
<div class="form-group">
<label class="form-label" for="timelockMinDelay">Минимальная задержка Timelock (в днях):</label>
<input type="number" id="timelockMinDelay" v-model="dleDeploymentSettings.timelockMinDelayDays" min="0" class="form-control">
</div>
<!-- 7. RPC Провайдеры -->
<h4>RPC Провайдеры</h4>
<p>Конфигурации RPC для сетей, которые будут использоваться в приложении.</p>
<!-- Список добавленных RPC -->
<div v-if="securitySettings.rpcConfigs.length > 0" class="rpc-list">
<h5>Добавленные RPC конфигурации:</h5>
<div v-for="(rpc, index) in securitySettings.rpcConfigs" :key="index" class="rpc-entry">
<span><strong>ID Сети:</strong> {{ rpc.networkId }}</span>
<span><strong>URL:</strong> {{ rpc.rpcUrl }}</span>
<div class="rpc-actions">
<button class="btn btn-info btn-sm" @click="testRpcHandler(rpc)" :disabled="testingRpc && testingRpcId === rpc.networkId">
<i class="fas" :class="testingRpc && testingRpcId === rpc.networkId ? 'fa-spinner fa-spin' : 'fa-check-circle'"></i>
{{ testingRpc && testingRpcId === rpc.networkId ? 'Проверка...' : 'Тест' }}
</button>
<button class="btn btn-danger btn-sm" @click="removeRpcConfig(index)">
<i class="fas fa-trash"></i> Удалить
</button>
</div>
</div>
</div>
<p v-else>Нет добавленных RPC конфигураций.</p>
<!-- Форма добавления нового RPC -->
<div class="setting-form add-rpc-form">
<h5>Добавить новую RPC конфигурацию:</h5>
<div class="form-group">
<label class="form-label" for="newRpcNetworkId">ID Сети:</label>
<select id="newRpcNetworkId" v-model="networkEntry.networkId" class="form-control">
<optgroup v-for="(group, groupIndex) in networkGroups" :key="groupIndex" :label="group.label">
<option v-for="option in group.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</optgroup>
</select>
<div v-if="networkEntry.networkId === 'custom'" class="mt-2">
<label class="form-label" for="customNetworkId">Пользовательский ID:</label>
<input type="text" id="customNetworkId" v-model="networkEntry.customNetworkId" class="form-control" placeholder="Введите ID сети">
<label class="form-label mt-2" for="customChainId">Chain ID:</label>
<input type="number" id="customChainId" v-model="networkEntry.customChainId" class="form-control" placeholder="Например, 1 для Ethereum">
<small>Chain ID - уникальный идентификатор блокчейн-сети (целое число)</small>
</div>
<small>ID сети должен совпадать со значением в выпадающем списке сетей при создании DLE</small>
</div>
<div class="form-group">
<label class="form-label" for="newRpcUrl">RPC URL:</label>
<input type="text" id="newRpcUrl" v-model="networkEntry.rpcUrl" class="form-control" placeholder="https://...">
<!-- Предложение URL на основе выбранной сети -->
<small v-if="defaultRpcUrlSuggestion" class="suggestion">
Предложение: {{ defaultRpcUrlSuggestion }}
<button class="btn-link" @click="useDefaultRpcUrl">Использовать</button>
</small>
</div>
<button class="btn btn-secondary" @click="addRpcConfig">Добавить RPC</button>
</div>
<!-- 8. Выбор сети для деплоя -->
<h4>Сеть для деплоя</h4>
<div class="form-group">
<label class="form-label" for="deployNetwork">Выберите сеть блокчейн для деплоя:</label>
<select id="deployNetwork" v-model="dleDeploymentSettings.blockchainNetwork" class="form-control">
<option v-if="loadingNetworks" disabled>Загрузка сетей...</option>
<option v-for="network in networks" :key="network.value" :value="network.value">
{{ network.label }}
</option>
</select>
<small class="text-warning" v-if="!dleDeploymentSettings.blockchainNetwork.includes('testnet') &&
!['sepolia', 'goerli', 'mumbai'].includes(dleDeploymentSettings.blockchainNetwork)">
<i class="fas fa-exclamation-triangle"></i>
Внимание! Для тестирования рекомендуется использовать тестовые сети (Sepolia, Goerli, Mumbai).
</small>
</div>
<!-- 9. Ключ Деплоера -->
<h4>Ключ Деплоера</h4>
<div class="form-group">
<label class="form-label" for="deployerKey">Приватный ключ для деплоя:</label>
<div class="input-group">
<input :type="showDeployerKey ? 'text' : 'password'" id="deployerKey" v-model="securitySettings.deployerPrivateKey" class="form-control">
<button class="btn btn-outline-secondary" @click="toggleShowDeployerKey">
<i :class="showDeployerKey ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</button>
</div>
</div>
<!-- 10. Газовые настройки -->
<div class="form-group">
<h4>Газовые настройки</h4>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customGas" v-model="useCustomGas">
<label class="custom-control-label" for="customGas">Использовать пользовательские настройки газа</label>
</div>
<div v-if="useCustomGas" class="gas-settings mt-3">
<div class="form-group">
<label for="gasLimit">Лимит газа (Gas Limit):</label>
<input type="number" id="gasLimit" v-model="gasSettings.gasLimit" class="form-control">
</div>
<div class="form-group">
<label for="maxFeePerGas">Максимальная комиссия (Max Fee, gwei):</label>
<input type="number" id="maxFeePerGas" v-model="gasSettings.maxFeePerGas" class="form-control">
</div>
<div class="form-group">
<label for="maxPriorityFee">Приоритетная комиссия (Priority Fee, gwei):</label>
<input type="number" id="maxPriorityFee" v-model="gasSettings.maxPriorityFee" class="form-control">
</div>
</div>
</div>
<!-- 10. Кнопка деплоя DLE -->
<div class="deployment-actions mt-4">
<button class="btn btn-primary" @click="deployDLE" :disabled="isDeploying">
<i class="fas fa-rocket"></i> {{ isDeploying ? 'Создание DLE...' : 'Создать и задеплоить DLE (Digital Legal Entity)' }}
</button>
<!-- Результат деплоя -->
<div v-if="deployResult" class="deploy-result mt-3 alert alert-success">
<h5>DLE успешно создано!</h5>
<p><strong>Адрес токена:</strong> {{ deployResult.data?.tokenAddress }}</p>
<p><strong>Адрес таймлока:</strong> {{ deployResult.data?.timelockAddress }}</p>
<p><strong>Адрес контракта Governor:</strong> {{ deployResult.data?.governorAddress }}</p>
</div>
<!-- Ошибка деплоя -->
<div v-if="deployError" class="deploy-error mt-3 alert alert-danger">
<h5>Ошибка при создании DLE</h5>
<p>{{ deployError }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, onMounted, computed, ref, watch } from 'vue';
import axios from 'axios'; // Предполагаем, что axios доступен
import { useAuthContext } from '@/composables/useAuth'; // Импортируем composable useAuth
import dleService from '@/services/dleService';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks'; // Импортируем composable для работы с сетями
// TODO: Импортировать API
const { address, isAdmin, auth, user } = useAuthContext(); // Получаем объект адреса и статус админа
// Инициализация composable для работы с сетями блокчейн
const {
networkGroups,
networkEntry,
defaultRpcUrlSuggestion,
useDefaultRpcUrl,
validateAndPrepareNetworkConfig,
resetNetworkEntry,
testRpcConnection,
testingRpc,
testingRpcId,
networks,
fetchNetworks,
loadingNetworks
} = useBlockchainNetworks();
// Добавляем настройки безопасности и подключения
const securitySettings = reactive({
rpcConfigs: [], // Массив для хранения { networkId: string, rpcUrl: string, chainId: number }
deployerPrivateKey: '',
});
// Функция добавления новой RPC конфигурации
const addRpcConfig = () => {
const result = validateAndPrepareNetworkConfig();
if (!result.valid) {
alert(result.error);
return;
}
const { networkId, rpcUrl, chainId } = result.networkConfig;
// Проверка на дубликат ID
if (securitySettings.rpcConfigs.some(rpc => rpc.networkId === networkId)) {
alert(`Ошибка: RPC конфигурация для сети с ID '${networkId}' уже существует.`);
return;
}
securitySettings.rpcConfigs.push({ networkId, rpcUrl, chainId });
// Очистка полей ввода
resetNetworkEntry();
};
// Функция удаления RPC конфигурации
const removeRpcConfig = (index) => {
securitySettings.rpcConfigs.splice(index, 1);
};
const settings = reactive({
// contractAddress: '', // Удалено
// quorumPercent: 51, // Удалено
// rwaEnabled: false // Удалено
});
const dleDeploymentSettings = reactive({
name: '',
symbol: '',
partners: [{ address: '', amount: 1 }], // Начинаем с одного партнера для примера
proposalThreshold: 0, // Голосов для создания предложения
quorumPercent: 4, // Процент для кворума Governor
votingDelayDays: 1, // в днях
votingPeriodDays: 7, // в днях
timelockMinDelayDays: 2, // в днях
blockchainNetwork: 'polygon', // Значение по умолчанию
locationIndex: '',
locationCountry: '',
locationCity: '',
locationStreet: '',
locationHouse: '',
locationOffice: '',
selectedIsicCodes: [], // <<< Для хранения массива выбранных кодов ISIC
});
// Добавляем переменную useCustomGas для управления отображением пользовательских настроек газа
const useCustomGas = ref(false);
// Объявляем gasSettings как ref для корректного реактивного доступа в шаблоне
const gasSettings = reactive({
gasLimit: 3000000,
maxFeePerGas: 30,
maxPriorityFee: 2
});
// --- Состояние для загрузки и опций ISIC ---
const sectionOptions = ref([]);
const divisionOptions = ref([]);
const groupOptions = ref([]);
const classOptions = ref([]);
const isLoadingSections = ref(false);
const isLoadingDivisions = ref(false);
const isLoadingGroups = ref(false);
const isLoadingClasses = ref(false);
// --- Состояние для проверки адреса Nominatim ---
const isAddressVerifying = ref(false);
const addressVerificationResult = ref(null); // null, 'verified_exact', 'verified_street', 'verified_city', 'verified_ambiguous', 'not_found', 'ambiguous', 'error'
const verifiedAddressDetails = ref(null);
const isFetchingByZipcode = ref(false);
const addressFieldsVisible = ref(false);
// --- Состояние для выбранных значений на каждом уровне ISIC ---
const selectedSection = ref('');
const selectedDivision = ref('');
const selectedGroup = ref('');
const selectedClass = ref('');
// --- Для хранения текущего самого детализированного выбора ISIC до добавления в список ---
const currentSelectedIsicCode = ref('');
const currentSelectedIsicText = ref('');
// --- Отслеживание изменения страны ---
watch(() => dleDeploymentSettings.locationCountry, (newCountry, oldCountry) => {
if (newCountry !== oldCountry) {
console.log(`[BlockchainSettingsView] Страна изменена на: ${newCountry}. Очистка кодов деятельности.`);
selectedSection.value = ''; // Это вызовет каскадную очистку и сброс currentSelectedIsicCode
dleDeploymentSettings.selectedIsicCodes = []; // Очищаем также список уже добавленных кодов
fetchIsicCodes({ level: 1 }, sectionOptions, isLoadingSections);
}
});
// --- Функция для загрузки кодов ISIC из API ---
const fetchIsicCodes = async (params = {}, optionsRef, loadingRef) => {
if (!optionsRef || !loadingRef) {
console.error('[BlockchainSettingsView] fetchIsicCodes requires optionsRef and loadingRef');
return;
}
loadingRef.value = true;
optionsRef.value = []; // Очищаем перед загрузкой
try {
const queryParams = new URLSearchParams(params).toString();
console.debug(`[BlockchainSettingsView] Fetching ISIC codes with params: ${queryParams}`);
// Убедитесь, что базовый URL настроен правильно (например, через axios interceptors или .env)
const response = await axios.get(`/api/isic/codes?${queryParams}`);
if (response.data && Array.isArray(response.data.codes)) {
optionsRef.value = response.data.codes.map(code => ({
value: code.code,
// Отображаем код и описание для ясности
text: `${code.code} - ${code.description}`
}));
console.debug(`[BlockchainSettingsView] Loaded ISIC codes for level ${params.level || ('parent: '+params.parent_code)}, count:`, optionsRef.value.length);
} else {
console.error('[BlockchainSettingsView] Invalid response structure for ISIC codes:', response.data);
}
} catch (error) {
console.error('[BlockchainSettingsView] Error fetching ISIC codes:', error.response?.data || error.message);
// TODO: Показать пользователю уведомление об ошибке
} finally {
loadingRef.value = false;
}
};
// --- Функция для обновления информации о текущем полном выбранном коде ISIC ---
const updateCurrentIsicSelection = () => {
let code = '';
let text = '';
let optionsToSearch = [];
let valueToFind = '';
if (selectedClass.value) {
code = selectedClass.value;
optionsToSearch = classOptions.value;
valueToFind = selectedClass.value;
} else if (selectedGroup.value) {
code = selectedGroup.value;
optionsToSearch = groupOptions.value;
valueToFind = selectedGroup.value;
} else if (selectedDivision.value) {
code = selectedDivision.value;
optionsToSearch = divisionOptions.value;
valueToFind = selectedDivision.value;
} else if (selectedSection.value) {
code = selectedSection.value;
optionsToSearch = sectionOptions.value;
valueToFind = selectedSection.value;
}
if (code && optionsToSearch.length > 0 && valueToFind) {
const foundOption = optionsToSearch.find(opt => opt.value === valueToFind);
if (foundOption) {
text = foundOption.text;
}
}
currentSelectedIsicCode.value = code;
currentSelectedIsicText.value = text;
};
// --- Наблюдатели для каскадной загрузки и обновления текущего выбора ---
watch(selectedSection, (newVal) => {
selectedDivision.value = ''; divisionOptions.value = [];
selectedGroup.value = ''; groupOptions.value = [];
selectedClass.value = ''; classOptions.value = [];
if (newVal) {
fetchIsicCodes({ parent_code: newVal }, divisionOptions, isLoadingDivisions);
}
updateCurrentIsicSelection();
});
watch(selectedDivision, (newVal) => {
selectedGroup.value = ''; groupOptions.value = [];
selectedClass.value = ''; classOptions.value = [];
if (newVal) {
fetchIsicCodes({ parent_code: newVal }, groupOptions, isLoadingGroups);
}
updateCurrentIsicSelection();
});
watch(selectedGroup, (newVal) => {
selectedClass.value = ''; classOptions.value = [];
if (newVal) {
fetchIsicCodes({ parent_code: newVal }, classOptions, isLoadingClasses);
}
updateCurrentIsicSelection();
});
watch(selectedClass, () => {
updateCurrentIsicSelection();
});
// --- Начальная загрузка данных ---
onMounted(() => {
fetchIsicCodes({ level: 1 }, sectionOptions, isLoadingSections);
fetchNetworks(); // Загружаем список сетей для деплоя
// Автоподстановка адреса авторизированного пользователя в первого партнера, если есть права админа
if (address.value && isAdmin.value && dleDeploymentSettings.partners.length > 0) {
dleDeploymentSettings.partners[0].address = address.value;
}
// Слушаем изменения адреса авторизированного пользователя
watch(address, (newAddress) => {
if (newAddress && isAdmin.value && dleDeploymentSettings.partners.length > 0) {
dleDeploymentSettings.partners[0].address = newAddress;
}
});
// Загрузка настроек RPC с сервера
loadRpcSettings();
});
const totalInitialSupply = computed(() => {
return dleDeploymentSettings.partners.reduce((sum, partner) => sum + (Number(partner.amount) || 0), 0);
});
const addPartner = () => {
dleDeploymentSettings.partners.push({ address: '', amount: 1 });
};
const removePartner = (index) => {
dleDeploymentSettings.partners.splice(index, 1);
};
const loadBlockchainSettings = async () => {
console.log('[BlockchainSettingsView] Загрузка настроек блокчейна...');
// TODO: API call - Больше нет общих настроек для загрузки в 'settings'.
// Возможно, потребуется загрузить dleDeploymentSettings, если они сохраняются.
};
const saveSettings = async (section) => {
console.log(`[BlockchainSettingsView] Сохранение настроек раздела: ${section}`);
// TODO: API call - Функция saveSettings, вероятно, больше не нужна в текущем виде,
// так как нет общих настроек для сохранения. Деплой DLE обрабатывается отдельно.
// Если настройки DLE (dleDeploymentSettings) нужно сохранять без деплоя, нужна другая логика.
};
const isDeploying = ref(false);
const deployResult = ref(null);
const deployError = ref(null);
const formattedDLEParams = computed(() => {
// Преобразуем партнеров в формат для API
const partners = dleDeploymentSettings.partners.map(p => p.address);
const amounts = dleDeploymentSettings.partners.map(p => p.amount.toString());
// Формируем полный адрес
const location = [
dleDeploymentSettings.locationIndex,
dleDeploymentSettings.locationCountry,
dleDeploymentSettings.locationCity,
dleDeploymentSettings.locationStreet,
dleDeploymentSettings.locationHouse,
dleDeploymentSettings.locationOffice
].filter(Boolean).join(', ');
// Формируем коды ISIC
const isicCodes = dleDeploymentSettings.selectedIsicCodes.map(isic => isic.code);
return {
name: dleDeploymentSettings.name,
symbol: dleDeploymentSettings.symbol,
location,
isicCodes,
partners,
amounts,
network: dleDeploymentSettings.blockchainNetwork, // Добавляем выбранную сеть в параметры
minTimelockDelay: dleDeploymentSettings.timelockMinDelayDays,
votingDelay: Math.round(dleDeploymentSettings.votingDelayDays * 24 * 60 * 60 / 13), // конвертируем дни в блоки (13 секунд на блок)
votingPeriod: Math.round(dleDeploymentSettings.votingPeriodDays * 24 * 60 * 60 / 13), // конвертируем дни в блоки
proposalThreshold: dleDeploymentSettings.proposalThreshold,
quorumPercentage: dleDeploymentSettings.quorumPercent,
privateKey: securitySettings.deployerPrivateKey
};
});
const deployDLE = async () => {
isDeploying.value = true;
deployResult.value = null;
deployError.value = null;
try {
// Проверяем валидность формы
if (!validateDLEForm()) {
isDeploying.value = false;
return;
}
// Сначала сохраняем настройки RPC
const rpcSaved = await saveRpcSettings();
if (!rpcSaved) {
// Если не удалось сохранить, спрашиваем пользователя, хочет ли он продолжить
if (!confirm('Не удалось сохранить RPC настройки. Продолжить деплой DLE?')) {
isDeploying.value = false;
return;
}
}
// Отправляем запрос на создание DLE
const result = await dleService.createDLE(formattedDLEParams.value);
deployResult.value = result;
alert('DLE успешно создано!');
} catch (error) {
console.error('Ошибка при деплое DLE:', error);
deployError.value = error.response?.data?.message || error.message || 'Произошла ошибка при деплое DLE';
alert(deployError.value);
} finally {
isDeploying.value = false;
}
};
const validateDLEForm = () => {
// Проверяем обязательные поля
if (!dleDeploymentSettings.name) {
alert('Необходимо указать имя DLE');
return false;
}
if (!dleDeploymentSettings.symbol) {
alert('Необходимо указать символ токена');
return false;
}
// Проверяем выбрана ли сеть для деплоя
if (!dleDeploymentSettings.blockchainNetwork) {
alert('Необходимо выбрать сеть для деплоя');
return false;
}
// Проверяем адреса партнеров
for (const partner of dleDeploymentSettings.partners) {
if (!partner.address || !partner.address.startsWith('0x') || partner.address.length !== 42) {
alert('Некорректный адрес партнера');
return false;
}
if (!partner.amount || partner.amount <= 0) {
alert('Сумма токенов должна быть больше 0');
return false;
}
}
return true;
};
// --- Функция для поиска адреса по индексу через Nominatim ---
const fetchAddressByZipcode = async () => {
const zipcode = dleDeploymentSettings.locationIndex.trim();
if (!zipcode) {
return;
}
isFetchingByZipcode.value = true;
addressVerificationResult.value = null;
verifiedAddressDetails.value = null;
try {
const params = new URLSearchParams();
params.append('postalcode', zipcode);
params.append('format', 'jsonv2');
params.append('addressdetails', '1');
params.append('limit', '1');
const countryInput = dleDeploymentSettings.locationCountry.trim();
if (countryInput) {
if (countryInput.length === 2) {
params.append('countrycodes', countryInput.toUpperCase());
} else {
params.append('country', countryInput);
}
}
console.log(`[FetchByZipcode] Querying backend proxy for Nominatim with: ${params.toString()}`);
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
if (response.data && response.data.length > 0) {
const bestMatch = response.data[0];
console.log('[FetchByZipcode] Nominatim result:', bestMatch);
if (bestMatch.address) {
if (bestMatch.address.country) {
dleDeploymentSettings.locationCountry = bestMatch.address.country;
}
if (bestMatch.address.city) {
dleDeploymentSettings.locationCity = bestMatch.address.city;
} else if (bestMatch.address.town) {
dleDeploymentSettings.locationCity = bestMatch.address.town;
} else if (bestMatch.address.village) {
dleDeploymentSettings.locationCity = bestMatch.address.village;
} else {
// Город не найден четко, можно оставить поле пустым или сообщить
}
// Можно также попробовать заполнить регион/область, если есть такое поле
// dleDeploymentSettings.locationState = bestMatch.address.state;
addressFieldsVisible.value = true;
} else {
addressFieldsVisible.value = false;
}
} else {
addressFieldsVisible.value = false;
}
} catch (error) {
console.error('[FetchByZipcode] Error fetching address by zipcode:', error.response?.data || error.message);
addressFieldsVisible.value = false;
} finally {
isFetchingByZipcode.value = false;
}
};
// Дополнительная функция для скрытия полей, если индекс очищен
const checkIndexInput = () => {
if (!dleDeploymentSettings.locationIndex.trim()) {
addressFieldsVisible.value = false;
// Опционально: также можно очистить dleDeploymentSettings.locationCountry, city и т.д.
// dleDeploymentSettings.locationCountry = '';
// dleDeploymentSettings.locationCity = '';
}
};
// --- Функция для сборки строки адреса для Nominatim (для кнопки "Проверить адрес") ---
const buildAddressQuery = () => {
const parts = [];
let streetHouse = '';
const street = dleDeploymentSettings.locationStreet.trim();
const house = dleDeploymentSettings.locationHouse.trim().toLowerCase(); // Приводим номер дома к нижнему регистру
if (street) {
streetHouse += street;
}
if (house) {
streetHouse += (streetHouse ? ' ' : '') + house;
}
if (streetHouse) {
parts.push(streetHouse);
}
const city = dleDeploymentSettings.locationCity.trim();
if (city) {
parts.push(city);
}
const country = dleDeploymentSettings.locationCountry.trim();
if (country) {
parts.push(country);
}
return parts.join(', ');
};
// --- Функция для проверки адреса через Nominatim ---
const verifyAddress = async () => {
const addressQuery = buildAddressQuery();
if (!addressQuery.trim()) {
addressVerificationResult.value = null;
verifiedAddressDetails.value = null;
return;
}
isAddressVerifying.value = true;
addressVerificationResult.value = null;
verifiedAddressDetails.value = null;
try {
const params = new URLSearchParams({
q: addressQuery,
format: 'jsonv2',
addressdetails: 1,
limit: 5,
// countrycodes параметр убран, т.к. страна теперь свободный текстовый ввод
// Nominatim попытается определить ее из 'q'
});
console.log(`[VerifyAddress] Querying backend proxy for Nominatim with: ${params.toString()}`);
// Запрос теперь идет на ваш бэкенд-прокси
const response = await axios.get(`/api/geocoding/nominatim-search?${params.toString()}`);
// Ответ от бэкенд-прокси должен иметь ту же структуру, что и прямой ответ от Nominatim
if (response.data && Array.isArray(response.data)) { // Проверяем, что это массив (как отвечает Nominatim)
if (response.data.length > 0) {
const bestMatch = response.data[0];
verifiedAddressDetails.value = bestMatch;
console.log('[VerifyAddress] Nominatim best match via proxy:', bestMatch);
let countryMatches = true;
if (dleDeploymentSettings.locationCountry && bestMatch.address.country_code) {
if (dleDeploymentSettings.locationCountry.trim().toUpperCase() !== bestMatch.address.country_code.toUpperCase()) {
if (dleDeploymentSettings.locationCountry.length === 2) countryMatches = false;
}
} else if (dleDeploymentSettings.locationCountry && !bestMatch.address.country_code) {
countryMatches = false;
}
if (countryMatches) {
if (bestMatch.address.house_number && bestMatch.address.road) {
addressVerificationResult.value = 'verified_exact';
} else if (bestMatch.address.road) {
addressVerificationResult.value = 'verified_street';
} else if (bestMatch.address.city || bestMatch.address.town || bestMatch.address.village) {
addressVerificationResult.value = 'verified_city';
} else {
addressVerificationResult.value = 'verified_ambiguous';
}
} else {
addressVerificationResult.value = 'ambiguous';
}
} else {
// Nominatim вернул пустой массив - адрес не найден
addressVerificationResult.value = 'not_found';
}
} else {
// Ответ от бэкенд-прокси не в ожидаемом формате (не массив)
console.error('[VerifyAddress] Invalid response structure from backend proxy:', response.data);
addressVerificationResult.value = 'error'; // или более специфичная ошибка
verifiedAddressDetails.value = response.data; // Сохраняем то, что пришло, для отладки
}
} catch (error) {
console.error('[VerifyAddress] Error verifying address via backend proxy:', error.response?.data || error.message);
verifiedAddressDetails.value = error.response?.data; // Сохраняем детали ошибки для отладки
addressVerificationResult.value = 'error';
} finally {
isAddressVerifying.value = false;
}
};
// --- Функции для управления списком выбранных кодов ISIC ---
const addIsicCode = () => {
if (currentSelectedIsicCode.value && currentSelectedIsicText.value) {
const alreadyExists = dleDeploymentSettings.selectedIsicCodes.find(c => c.code === currentSelectedIsicCode.value);
if (!alreadyExists) {
dleDeploymentSettings.selectedIsicCodes.push({
code: currentSelectedIsicCode.value,
text: currentSelectedIsicText.value
});
// Сбрасываем селекторы для выбора следующего кода
selectedSection.value = ''; // Это вызовет каскадную очистку
// currentSelectedIsicCode.value = ''; // Уже сбросится через watch(selectedSection)
// currentSelectedIsicText.value = '';
} else {
alert('Этот код уже добавлен.');
}
} else {
alert('Код не выбран полностью.');
}
};
const removeIsicCode = (index) => {
dleDeploymentSettings.selectedIsicCodes.splice(index, 1);
};
const showDeployerKey = ref(false);
const toggleShowDeployerKey = () => {
showDeployerKey.value = !showDeployerKey.value;
};
// Функция загрузки настроек RPC с сервера
const loadRpcSettings = async () => {
try {
const response = await axios.get('/api/settings/rpc');
console.log('Ответ сервера на /api/settings/rpc:', response.data);
if (response.data && response.data.success) {
securitySettings.rpcConfigs = (response.data.data || []).map(rpc => ({
networkId: rpc.network_id,
rpcUrl: rpc.rpc_url,
chainId: rpc.chain_id
}));
console.log('[BlockchainSettingsView] RPC конфигурации успешно загружены:', securitySettings.rpcConfigs);
}
} catch (error) {
console.error('[BlockchainSettingsView] Ошибка при загрузке RPC конфигураций:', error);
}
};
// Функция сохранения настроек RPC на сервер
const saveRpcSettings = async () => {
try {
console.log('Отправляемые RPC:', securitySettings.rpcConfigs);
const response = await axios.post('/api/settings/rpc', {
rpcConfigs: JSON.parse(JSON.stringify(securitySettings.rpcConfigs))
});
if (response.data && response.data.success) {
console.log('[BlockchainSettingsView] RPC конфигурации успешно сохранены');
return true;
} else {
console.error('[BlockchainSettingsView] Ошибка при сохранении RPC конфигураций:', response.data);
return false;
}
} catch (error) {
console.error('[BlockchainSettingsView] Ошибка при сохранении RPC конфигураций:', error);
return false;
}
};
const isSavingRpc = ref(false);
// Функция сохранения настроек RPC с обратной связью
const saveRpcSettingsWithFeedback = async () => {
isSavingRpc.value = true;
try {
const success = await saveRpcSettings();
if (success) {
alert('RPC настройки успешно сохранены.');
} else {
alert('Ошибка при сохранении RPC настроек.');
}
} catch (error) {
console.error('[BlockchainSettingsView] Ошибка при сохранении RPC настроек:', error);
alert(`Ошибка при сохранении: ${error.message || 'Неизвестная ошибка'}`);
} finally {
isSavingRpc.value = false;
}
};
// Определяем группы сетей для деплоя (исключаем локальные и пользовательские)
const deployNetworkGroups = computed(() => {
// Фильтруем группы, оставляя только основные и тестовые сети
return networkGroups.filter(group =>
group.label === 'Основные сети' || group.label === 'Тестовые сети'
);
});
const testingRpcIndex = ref(-1);
// Функция-обработчик для тестирования RPC соединения
const testRpcHandler = async (rpc) => {
try {
const result = await testRpcConnection(rpc.networkId, rpc.rpcUrl);
if (result.success) {
alert(result.message);
} else {
alert(`Ошибка при подключении к ${rpc.networkId}: ${result.error}`);
}
} catch (error) {
console.error('[BlockchainSettingsView] Ошибка при тестировании RPC:', error);
alert(`Ошибка при тестировании RPC: ${error.message || 'Неизвестная ошибка'}`);
}
};
</script>
<style scoped>
.settings-panel {
padding: var(--block-padding);
background-color: var(--color-light);
border-radius: var(--radius-md);
margin-top: var(--spacing-lg);
animation: fadeIn var(--transition-normal);
}
h3 {
margin-bottom: var(--spacing-md);
color: var(--color-primary);
}
.sub-settings-panel {
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-lg);
border-bottom: 1px dashed var(--color-grey-light);
}
.sub-settings-panel:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.setting-form {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.form-group {
margin-bottom: 0;
}
.form-label {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.form-control {
max-width: 500px;
}
.btn-primary {
align-self: flex-start;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.partner-entry {
border: 1px solid var(--color-grey-light);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
border-radius: var(--radius-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.partner-entry .form-group {
margin-bottom: 0; /* Убираем лишний отступ у вложенных групп */
}
.btn-danger.btn-sm {
align-self: flex-start;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.875rem; /* Меньший размер для кнопки удаления */
}
.btn-secondary {
align-self: flex-start;
margin-bottom: var(--spacing-md); /* Отступ после кнопки "Добавить партнера" */
}
.btn-lg {
padding: var(--spacing-md) var(--spacing-lg);
font-size: 1.125rem;
}
.address-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); /* Адаптивная сетка */
gap: var(--spacing-md);
/* margin-bottom: var(--spacing-lg); Убрано, т.к. есть блок верификации */
}
/* Можно добавить специфичные стили для полей адреса, если нужно */
.address-grid .form-group {
margin-bottom: 0; /* Убрать лишний отступ у полей в сетке */
}
.code-list {
margin-bottom: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.code-entry {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-grey-light);
border-radius: var(--radius-sm);
background-color: white;
}
.code-entry .btn-danger {
flex-shrink: 0;
margin-left: var(--spacing-md);
}
.add-code-form {
margin-top: var(--spacing-sm); /* Меньше отступ, т.к. он под списком */
}
.add-code-form .btn-secondary {
align-self: flex-start;
}
.address-verification-section {
margin-top: var(--spacing-xs); /* Небольшой отступ сверху */
margin-bottom: var(--spacing-lg); /* Отступ после секции верификации */
}
.verification-status p {
margin-bottom: var(--spacing-xs);
padding: var(--spacing-sm);
border-radius: var(--radius-sm);
}
.verification-status .alert-success {
background-color: var(--color-success-light, #e6fffa); /* Добавлены цвета по умолчанию */
color: var(--color-success-dark, #006d5b);
border: 1px solid var(--color-success-dark, #006d5b);
}
.verification-status .alert-warning {
background-color: var(--color-warning-light, #fff8e1);
color: var(--color-warning-dark, #8a6d3b);
border: 1px solid var(--color-warning-dark, #8a6d3b);
}
.verification-status .alert-danger {
background-color: var(--color-danger-light, #fdecea);
color: var(--color-danger-dark, #a94442);
border: 1px solid var(--color-danger-dark, #a94442);
}
.code-block {
background-color: #f5f5f5;
padding: var(--spacing-sm);
border-radius: var(--radius-sm);
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--color-grey-light, #e0e0e0);
}
.current-isic-selection {
padding: var(--spacing-sm);
background-color: var(--color-grey-x-light);
border-radius: var(--radius-sm);
margin-top: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.isic-codes-list ul {
list-style-type: none;
padding-left: 0;
}
.isic-codes-list li {
background-color: var(--color-background);
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--color-grey-light);
border-radius: var(--radius-sm);
}
.btn-xs {
padding: 0.2rem 0.4rem;
font-size: 0.75rem;
}
.rpc-list {
margin-bottom: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.rpc-entry {
background-color: var(--color-background);
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--color-grey-light);
border-radius: var(--radius-sm);
margin-bottom: var(--spacing-xs);
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.rpc-entry span {
flex-grow: 1;
}
.add-rpc-form {
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px dashed var(--color-grey-light);
}
.add-rpc-form h5 {
margin-bottom: var(--spacing-sm);
}
.deployment-actions {
margin-top: var(--spacing-md);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.deploy-result {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
border-radius: var(--radius-sm);
background-color: var(--color-success-light);
color: var(--color-success-dark);
border: 1px solid var(--color-success-dark);
}
.deploy-error {
margin-top: var(--spacing-md);
padding: var(--spacing-md);
border-radius: var(--radius-sm);
background-color: var(--color-danger-light);
color: var(--color-danger-dark);
border: 1px solid var(--color-danger-dark);
}
.suggestion {
background-color: rgba(76, 175, 80, 0.1);
border-left: 3px solid var(--color-primary, #4caf50);
padding: 6px 10px;
margin-top: 8px;
border-radius: 0 4px 4px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-link {
background: none;
border: none;
padding: 0;
color: var(--color-primary, #4caf50);
text-decoration: underline;
cursor: pointer;
font-size: 0.875rem;
}
.btn-link:hover {
color: var(--color-primary-dark, #388e3c);
text-decoration: none;
}
.mt-2 {
margin-top: 10px;
}
.text-warning {
color: #f57c00; /* оранжевый для предупреждений */
margin-top: 5px;
display: block;
}
.rpc-actions {
display: flex;
gap: 8px;
align-items: center;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn-info:hover:not(:disabled) {
background-color: #138496;
}
.btn-info:disabled {
background-color: #a0d2dc;
cursor: not-allowed;
}
</style>