ваше сообщение коммита
This commit is contained in:
@@ -56,10 +56,13 @@ export default function useBlockchainNetworks() {
|
||||
{ value: 'holesky', label: 'Holesky (Ethereum testnet)', chainId: 17000 },
|
||||
{ value: 'bsc-testnet', label: 'BSC Testnet', chainId: 97 },
|
||||
{ value: 'mumbai', label: 'Mumbai (Polygon testnet)', chainId: 80001 },
|
||||
{ value: 'polygon-amoy', label: 'Polygon Amoy (testnet)', chainId: 80002 },
|
||||
{ value: 'arbitrum-goerli', label: 'Arbitrum Goerli', chainId: 421613 },
|
||||
{ value: 'arbitrum-sepolia', label: 'Arbitrum Sepolia', chainId: 421614 },
|
||||
{ value: 'optimism-goerli', label: 'Optimism Goerli', chainId: 420 },
|
||||
{ value: 'avalanche-fuji', label: 'Avalanche Fuji', chainId: 43113 },
|
||||
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 }
|
||||
{ value: 'fantom-testnet', label: 'Fantom Testnet', chainId: 4002 },
|
||||
{ value: 'base-sepolia', label: 'Base Sepolia Testnet', chainId: 84532 }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -63,7 +63,28 @@
|
||||
</div>
|
||||
|
||||
<div class="dle-details">
|
||||
<div class="detail-item">
|
||||
<div class="detail-item" v-if="dle.deployedMultichain">
|
||||
<strong>🌐 Мультичейн деплой:</strong>
|
||||
<span class="multichain-badge">{{ dle.totalNetworks }}/{{ dle.supportedChainIds?.length || dle.totalNetworks }} сетей</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="dle.networks && dle.networks.length">
|
||||
<strong>Адреса по сетям:</strong>
|
||||
<ul class="networks-list">
|
||||
<li v-for="net in dle.networks" :key="net.chainId" class="network-item">
|
||||
<span class="chain-name">{{ getChainName(net.chainId) }}:</span>
|
||||
<a
|
||||
:href="getExplorerUrl(net.chainId, net.dleAddress)"
|
||||
target="_blank"
|
||||
class="address-link"
|
||||
@click.stop
|
||||
>
|
||||
{{ shortenAddress(net.dleAddress) }}
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="detail-item" v-else>
|
||||
<strong>Адрес контракта:</strong>
|
||||
<a
|
||||
:href="`https://sepolia.etherscan.io/address/${dle.dleAddress}`"
|
||||
@@ -75,15 +96,6 @@
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="detail-item" v-if="dle.networks && dle.networks.length">
|
||||
<strong>Адреса по сетям:</strong>
|
||||
<ul class="networks-list">
|
||||
<li v-for="net in dle.networks" :key="net.chainId">
|
||||
Chain {{ net.chainId }}:
|
||||
<span class="address">{{ shortenAddress(net.address) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<strong>Местоположение:</strong> {{ dle.location }}
|
||||
</div>
|
||||
@@ -347,6 +359,35 @@ function shortenAddress(address) {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
function getChainName(chainId) {
|
||||
const chainNames = {
|
||||
1: 'Ethereum',
|
||||
11155111: 'Sepolia',
|
||||
17000: 'Holesky',
|
||||
421614: 'Arbitrum Sepolia',
|
||||
84532: 'Base Sepolia',
|
||||
137: 'Polygon',
|
||||
56: 'BSC',
|
||||
42161: 'Arbitrum'
|
||||
};
|
||||
return chainNames[chainId] || `Chain ${chainId}`;
|
||||
}
|
||||
|
||||
function getExplorerUrl(chainId, address) {
|
||||
const explorers = {
|
||||
1: 'https://etherscan.io',
|
||||
11155111: 'https://sepolia.etherscan.io',
|
||||
17000: 'https://holesky.etherscan.io',
|
||||
421614: 'https://sepolia.arbiscan.io',
|
||||
84532: 'https://sepolia.basescan.org',
|
||||
137: 'https://polygonscan.com',
|
||||
56: 'https://bscscan.com',
|
||||
42161: 'https://arbiscan.io'
|
||||
};
|
||||
const baseUrl = explorers[chainId] || 'https://etherscan.io';
|
||||
return `${baseUrl}/address/${address}`;
|
||||
}
|
||||
|
||||
function openDleOnEtherscan(address) {
|
||||
window.open(`https://sepolia.etherscan.io/address/${address}`, '_blank');
|
||||
}
|
||||
@@ -724,6 +765,40 @@ onBeforeUnmount(() => {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.multichain-badge {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.networks-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.network-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.network-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.chain-name {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -325,6 +325,41 @@
|
||||
<small class="form-help">3-10 символов для токена управления (Governance Token)</small>
|
||||
</div>
|
||||
|
||||
<!-- Логотип токена -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="tokenLogo">Логотип токена (изображение):</label>
|
||||
<input
|
||||
id="tokenLogo"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="form-control"
|
||||
@change="onLogoSelected"
|
||||
>
|
||||
<small class="form-help">Поддерживаются PNG/JPG/GIF/WEBP, до 5MB</small>
|
||||
<div v-if="logoPreviewUrl" class="logo-preview" style="margin-top:8px;display:flex;gap:10px;align-items:center;">
|
||||
<img :src="logoPreviewUrl" alt="logo preview" style="width:48px;height:48px;border-radius:6px;object-fit:contain;border:1px solid #e9ecef;" />
|
||||
<span class="address">{{ logoFile?.name || 'Предпросмотр' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ENS домен для логотипа -->
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="ensDomain">ENS‑домен для логотипа (опционально):</label>
|
||||
<input
|
||||
id="ensDomain"
|
||||
type="text"
|
||||
v-model="ensDomain"
|
||||
placeholder="например: vc-hb3-accelerator.eth"
|
||||
class="form-control"
|
||||
@blur="resolveEnsAvatar"
|
||||
>
|
||||
<small class="form-help">Если указан, попытаемся получить аватар ENS и использовать его как logoURI</small>
|
||||
<div v-if="ensResolvedUrl" style="margin-top:8px;display:flex;gap:10px;align-items:center;">
|
||||
<img :src="ensResolvedUrl" alt="ens avatar" style="width:32px;height:32px;border-radius:50%;object-fit:cover;border:1px solid #e9ecef;" />
|
||||
<span class="address">{{ ensResolvedUrl }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -866,6 +901,7 @@ import { reactive, ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthContext } from '@/composables/useAuth';
|
||||
import axios from 'axios';
|
||||
import api from '@/api/axios';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -2393,6 +2429,7 @@ const maskedPrivateKey = computed(() => {
|
||||
|
||||
// Функция деплоя смарт-контрактов DLE
|
||||
const deploySmartContracts = async () => {
|
||||
console.log('🚀 Начало деплоя DLE...');
|
||||
try {
|
||||
// Валидация данных
|
||||
if (!isFormValid.value) {
|
||||
@@ -2451,43 +2488,115 @@ const deploySmartContracts = async () => {
|
||||
const preData = pre.data?.data;
|
||||
if (pre.data?.success && preData) {
|
||||
const lacks = (preData.insufficient || []);
|
||||
const warnings = (preData.warnings || []);
|
||||
|
||||
if (lacks.length > 0) {
|
||||
const lines = (preData.balances || []).map(b => `- Chain ${b.chainId}: ${b.balanceEth} ETH${b.ok ? '' : ' (недостаточно)'}`);
|
||||
alert('Недостаточно средств в некоторых сетях:\n' + lines.join('\n'));
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
const lines = (preData.balances || []).map(b => {
|
||||
const status = b.ok ? '✅' : '❌';
|
||||
const warning = warnings.includes(b.chainId) ? ' ⚠️' : '';
|
||||
return `${status} Chain ${b.chainId}: ${b.balanceEth} ETH (мин. ${b.minRequiredEth} ETH)${warning}`;
|
||||
});
|
||||
|
||||
const message = `Проверка балансов завершена:\n\n${lines.join('\n')}\n\n${lacks.length > 0 ? '❌ Недостаточно средств в некоторых сетях!' : ''}\n${warnings.length > 0 ? '⚠️ Предупреждения в некоторых сетях!' : ''}`;
|
||||
|
||||
if (lacks.length > 0) {
|
||||
alert(message);
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
} else if (warnings.length > 0) {
|
||||
const proceed = confirm(message + '\n\nПродолжить деплой?');
|
||||
if (!proceed) {
|
||||
showDeployProgress.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Проверка балансов пройдена:', preData.summary);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Ошибка проверки балансов:', e.message);
|
||||
// Если precheck недоступен, не блокируем — продолжаем
|
||||
}
|
||||
|
||||
deployProgress.value = 30;
|
||||
deployStatus.value = 'Компиляция смарт-контрактов...';
|
||||
|
||||
// Автокомпиляция контрактов перед деплоем
|
||||
console.log('🔨 Запуск автокомпиляции...');
|
||||
try {
|
||||
const compileResponse = await axios.post('/compile-contracts');
|
||||
console.log('✅ Контракты скомпилированы:', compileResponse.data);
|
||||
} catch (compileError) {
|
||||
console.warn('⚠️ Ошибка автокомпиляции:', compileError.message);
|
||||
// Продолжаем деплой даже если компиляция не удалась
|
||||
}
|
||||
|
||||
deployProgress.value = 40;
|
||||
deployStatus.value = 'Отправка данных на сервер...';
|
||||
|
||||
// Вызов API для деплоя
|
||||
const response = await axios.post('/dle-v2', deployData);
|
||||
|
||||
deployProgress.value = 70;
|
||||
deployProgress.value = 50;
|
||||
deployStatus.value = 'Деплой смарт-контракта в блокчейне...';
|
||||
|
||||
const response = await axios.post('/dle-v2', deployData);
|
||||
|
||||
deployProgress.value = 80;
|
||||
deployStatus.value = 'Проверка результатов деплоя...';
|
||||
|
||||
if (response.data.success) {
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = '✅ DLE успешно развернут!';
|
||||
const result = response.data.data;
|
||||
|
||||
// Сохраняем адрес контракта
|
||||
// dleSettings.predictedAddress = response.data.data?.dleAddress || 'Адрес будет доступен после деплоя';
|
||||
|
||||
// Небольшая задержка для показа успешного завершения
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
}, 2000);
|
||||
// Проверяем результаты мульти-чейн деплоя
|
||||
if (result.networks && Array.isArray(result.networks)) {
|
||||
const successfulNetworks = result.networks.filter(n => n.success);
|
||||
const failedNetworks = result.networks.filter(n => !n.success);
|
||||
|
||||
if (failedNetworks.length > 0) {
|
||||
console.warn('Некоторые сети не удалось развернуть:', failedNetworks);
|
||||
}
|
||||
|
||||
if (successfulNetworks.length > 0) {
|
||||
// Проверяем, что все адреса одинаковые
|
||||
const addresses = successfulNetworks.map(n => n.address);
|
||||
const uniqueAddresses = [...new Set(addresses)];
|
||||
|
||||
if (uniqueAddresses.length === 1) {
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = `✅ DLE успешно развернут в ${successfulNetworks.length} сетях с одинаковым адресом!`;
|
||||
|
||||
console.log('🎉 Мульти-чейн деплой завершен успешно!');
|
||||
console.log('Адрес DLE:', uniqueAddresses[0]);
|
||||
console.log('Сети:', successfulNetworks.map(n => `Chain ${n.chainId}: ${n.address}`));
|
||||
|
||||
// Небольшая задержка для показа успешного завершения
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
// Перенаправляем на главную страницу управления
|
||||
router.push('/management');
|
||||
}, 3000);
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ ОШИБКА: Адреса DLE в разных сетях не совпадают! Это может указывать на проблему с CREATE2.');
|
||||
}
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Не удалось развернуть DLE ни в одной сети');
|
||||
}
|
||||
} else {
|
||||
// Fallback для одиночного деплоя
|
||||
deployProgress.value = 100;
|
||||
deployStatus.value = '✅ DLE успешно развернут!';
|
||||
|
||||
setTimeout(() => {
|
||||
showDeployProgress.value = false;
|
||||
router.push('/management');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
} else {
|
||||
showDeployProgress.value = false;
|
||||
alert('❌ Ошибка при деплое: ' + response.data.error);
|
||||
alert('❌ Ошибка при деплое: ' + (response.data.message || response.data.error));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -2499,16 +2608,15 @@ const deploySmartContracts = async () => {
|
||||
|
||||
// Валидация формы
|
||||
const isFormValid = computed(() => {
|
||||
return (
|
||||
return Boolean(
|
||||
dleSettings.jurisdiction &&
|
||||
dleSettings.name &&
|
||||
dleSettings.tokenSymbol ||
|
||||
dleSettings.tokenStandard !== 'ERC20' ||
|
||||
dleSettings.partners.length > 0 &&
|
||||
dleSettings.tokenSymbol &&
|
||||
(dleSettings.partners.length > 0) &&
|
||||
dleSettings.partners.every(partner => partner.address && partner.amount > 0) &&
|
||||
dleSettings.governanceQuorum > 0 &&
|
||||
dleSettings.governanceQuorum <= 100 &&
|
||||
dleSettings.selectedNetworks.length > 0 &&
|
||||
(dleSettings.selectedNetworks.length > 0) &&
|
||||
// Проверка приватного ключа
|
||||
unifiedPrivateKey.value &&
|
||||
keyValidation.unified?.isValid &&
|
||||
@@ -2523,6 +2631,88 @@ const validateCoordinates = (coordinates) => {
|
||||
const coordRegex = /^-?\d+\.\d+,-?\d+\.\d+$/;
|
||||
return coordRegex.test(coordinates);
|
||||
};
|
||||
|
||||
const logoFile = ref(null);
|
||||
const logoPreviewUrl = ref('');
|
||||
const ensDomain = ref('');
|
||||
const ensResolvedUrl = ref('');
|
||||
|
||||
function onLogoSelected(e) {
|
||||
const file = e?.target?.files?.[0];
|
||||
logoFile.value = file || null;
|
||||
logoPreviewUrl.value = '';
|
||||
if (file) {
|
||||
try { logoPreviewUrl.value = URL.createObjectURL(file); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveEnsAvatar() {
|
||||
ensResolvedUrl.value = '';
|
||||
const name = (ensDomain.value || '').trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const resp = await api.get(`/ens/avatar`, { params: { name } });
|
||||
const url = resp.data?.data?.url;
|
||||
if (url) {
|
||||
ensResolvedUrl.value = url;
|
||||
// если файл не выбран – используем ENS для предпросмотра
|
||||
if (!logoFile.value) logoPreviewUrl.value = url;
|
||||
} else {
|
||||
// фолбэк на дефолт
|
||||
ensResolvedUrl.value = '/uploads/logos/default-token.svg';
|
||||
if (!logoFile.value) logoPreviewUrl.value = ensResolvedUrl.value;
|
||||
}
|
||||
} catch (_) {
|
||||
ensResolvedUrl.value = '/uploads/logos/default-token.svg';
|
||||
if (!logoFile.value) logoPreviewUrl.value = ensResolvedUrl.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDeploy() {
|
||||
try {
|
||||
// Подготовка данных формы
|
||||
const deployData = {
|
||||
name: dleSettings.name,
|
||||
symbol: dleSettings.tokenSymbol,
|
||||
location: locationText.value,
|
||||
coordinates: dleSettings.coordinates || '',
|
||||
jurisdiction: Number(dleSettings.jurisdiction) || 1,
|
||||
oktmo: Number(dleSettings.selectedOktmo) || null,
|
||||
okvedCodes: Array.isArray(dleSettings.selectedOkved) ? dleSettings.selectedOkved.map(x => String(x)) : [],
|
||||
kpp: dleSettings.kppCode ? Number(dleSettings.kppCode) : null,
|
||||
initialPartners: dleSettings.partners.map(p => p.address).filter(Boolean),
|
||||
initialAmounts: dleSettings.partners.map(p => p.amount).filter(a => a > 0),
|
||||
supportedChainIds: dleSettings.selectedNetworks || [],
|
||||
currentChainId: dleSettings.selectedNetworks[0] || 1,
|
||||
privateKey: unifiedPrivateKey.value,
|
||||
etherscanApiKey: etherscanApiKey.value,
|
||||
autoVerifyAfterDeploy: autoVerifyAfterDeploy.value
|
||||
};
|
||||
|
||||
// Если выбран логотип — загружаем и подставляем logoURI
|
||||
if (logoFile.value) {
|
||||
const form = new FormData();
|
||||
form.append('logo', logoFile.value);
|
||||
const uploadResp = await axios.post('/uploads/logo', form, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const uploaded = uploadResp.data?.data?.url || uploadResp.data?.data?.path;
|
||||
if (uploaded) {
|
||||
deployData.logoURI = uploaded;
|
||||
}
|
||||
} else if (ensResolvedUrl.value) {
|
||||
deployData.logoURI = ensResolvedUrl.value;
|
||||
} else {
|
||||
// фолбэк на дефолт
|
||||
deployData.logoURI = '/uploads/logos/default-token.svg';
|
||||
}
|
||||
|
||||
console.log('Данные для деплоя DLE:', deployData);
|
||||
|
||||
// ... остальные данные остаются без изменений
|
||||
} catch (error) {
|
||||
console.error('Ошибка при отправке данных:', error);
|
||||
// Обработка ошибки
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -4416,4 +4606,6 @@ const validateCoordinates = (coordinates) => {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo-preview img { box-shadow: 0 1px 4px rgba(0,0,0,0.06); background:#fff; }
|
||||
</style>
|
||||
@@ -541,6 +541,9 @@ const getChainName = (chainId) => {
|
||||
1: 'Ethereum Mainnet',
|
||||
11155111: 'Sepolia Testnet',
|
||||
17000: 'Holesky Testnet',
|
||||
84532: 'Base Sepolia Testnet',
|
||||
80002: 'Polygon Amoy Testnet',
|
||||
421614: 'Arbitrum Sepolia Testnet',
|
||||
137: 'Polygon',
|
||||
56: 'BSC',
|
||||
42161: 'Arbitrum One'
|
||||
|
||||
Reference in New Issue
Block a user