562 lines
15 KiB
Vue
562 lines
15 KiB
Vue
<template>
|
||
<div class="contract-interaction">
|
||
<h2>Взаимодействие с контрактом</h2>
|
||
|
||
<div v-if="!isInitialized" class="loading-message">
|
||
Загрузка контракта...
|
||
</div>
|
||
|
||
<div v-if="!isConnected">
|
||
<button
|
||
@click="connectWallet"
|
||
class="connect-button"
|
||
>
|
||
Подключить кошелек
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="error" class="error-message">{{ error }}</div>
|
||
|
||
<div v-if="isConnected && isCorrectNetwork" class="wallet-info">
|
||
<p>Адрес кошелька: {{ address }}</p>
|
||
|
||
<div class="contract-controls">
|
||
<h3>Управление контрактом</h3>
|
||
<p v-if="currentPrice" class="price-info">
|
||
Текущая цена: {{ formatPrice(currentPrice) }} ETH
|
||
</p>
|
||
|
||
<!-- Панель управления для владельца -->
|
||
<div v-if="isOwner" class="owner-controls">
|
||
<h4>Панель владельца</h4>
|
||
<div class="input-group">
|
||
<input
|
||
v-model="newPrice"
|
||
type="number"
|
||
step="0.001"
|
||
placeholder="Новая цена (ETH)"
|
||
class="amount-input"
|
||
/>
|
||
<button
|
||
@click="handleSetPrice"
|
||
:disabled="!newPrice || isLoading"
|
||
class="admin-button"
|
||
>
|
||
Установить цену
|
||
</button>
|
||
</div>
|
||
<button
|
||
@click="handleWithdraw"
|
||
:disabled="isLoading"
|
||
class="admin-button withdraw-button"
|
||
>
|
||
Вывести средства
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Панель покупки -->
|
||
<div class="purchase-panel">
|
||
<h4>Покупка</h4>
|
||
<div class="input-group">
|
||
<input
|
||
v-model="amount"
|
||
type="number"
|
||
placeholder="Введите количество"
|
||
class="amount-input"
|
||
/>
|
||
<button
|
||
@click="handlePurchase"
|
||
:disabled="!amount || isLoading"
|
||
class="purchase-button"
|
||
>
|
||
{{ isLoading ? 'Обработка...' : 'Купить' }}
|
||
</button>
|
||
</div>
|
||
<p v-if="amount && currentPrice" class="total-cost">
|
||
Общая стоимость: {{ formatPrice(calculateTotalCost()) }} ETH
|
||
</p>
|
||
</div>
|
||
|
||
<p v-if="success" class="success-message">{{ success }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, computed, watch } from 'vue'
|
||
import { BrowserProvider, Contract, JsonRpcProvider, formatEther, parseEther } from 'ethers'
|
||
|
||
// Инициализируем все ref переменные в начале
|
||
const amount = ref('')
|
||
const newPrice = ref('')
|
||
const isLoading = ref(false)
|
||
const error = ref('')
|
||
const success = ref('')
|
||
const currentPrice = ref(null)
|
||
const contractOwner = ref(null)
|
||
const isCorrectNetwork = ref(false)
|
||
const isConnected = ref(false)
|
||
const isInitialized = ref(false)
|
||
const address = ref(null)
|
||
const walletProvider = ref(null)
|
||
const isAuthenticated = ref(false)
|
||
|
||
// Константы
|
||
const SEPOLIA_CHAIN_ID = 11155111
|
||
const provider = new JsonRpcProvider(import.meta.env.VITE_APP_ETHEREUM_NETWORK_URL)
|
||
const contractAddress = '0xFF7602583E82C097Ae548Fc8B894F0a73089985E'
|
||
const contractABI = [
|
||
'function purchase(uint256 amount) payable',
|
||
'function price() view returns (uint256)',
|
||
'function owner() view returns (address)',
|
||
'function setPrice(uint256 newPrice) public',
|
||
'function withdraw() public',
|
||
'event Purchase(address buyer, uint256 amount)'
|
||
]
|
||
|
||
// Вычисляемые свойства
|
||
const isOwner = computed(() => {
|
||
return address.value && contractOwner.value &&
|
||
address.value.toLowerCase() === contractOwner.value.toLowerCase()
|
||
})
|
||
|
||
// Функции
|
||
function formatPrice(price) {
|
||
if (!price) return '0'
|
||
return formatEther(price)
|
||
}
|
||
|
||
// Функция инициализации контракта
|
||
async function initializeContract() {
|
||
try {
|
||
if (!provider) {
|
||
throw new Error('Provider не доступен')
|
||
}
|
||
|
||
const contract = new Contract(contractAddress, contractABI, provider)
|
||
await Promise.all([
|
||
contract.price().then(price => {
|
||
currentPrice.value = price
|
||
console.log('Начальная цена:', formatEther(price), 'ETH')
|
||
}),
|
||
contract.owner().then(owner => {
|
||
contractOwner.value = owner
|
||
console.log('Владелец контракта:', owner)
|
||
})
|
||
])
|
||
|
||
isInitialized.value = true
|
||
} catch (err) {
|
||
console.error('Ошибка при инициализации контракта:', err)
|
||
error.value = 'Ошибка при инициализации контракта: ' + err.message
|
||
}
|
||
}
|
||
|
||
// Функция подключения к MetaMask
|
||
async function connectWallet() {
|
||
try {
|
||
// Проверяем доступность MetaMask
|
||
if (!window.ethereum) {
|
||
throw new Error('MetaMask не установлен')
|
||
}
|
||
|
||
// Запрашиваем доступ к аккаунту
|
||
const accounts = await window.ethereum.request({
|
||
method: 'eth_requestAccounts'
|
||
})
|
||
|
||
// Сохраняем адрес и провайдер
|
||
address.value = accounts[0]
|
||
walletProvider.value = window.ethereum
|
||
isConnected.value = true
|
||
|
||
// Подписываемся на изменение аккаунта
|
||
window.ethereum.on('accountsChanged', handleAccountsChanged)
|
||
window.ethereum.on('chainChanged', handleChainChanged)
|
||
|
||
await checkNetwork()
|
||
} catch (err) {
|
||
console.error('Ошибка при подключении кошелька:', err)
|
||
error.value = 'Ошибка при подключении кошелька: ' + err.message
|
||
}
|
||
}
|
||
|
||
// Обработчики событий MetaMask
|
||
function handleAccountsChanged(accounts) {
|
||
if (accounts.length === 0) {
|
||
// MetaMask отключен
|
||
isConnected.value = false
|
||
address.value = null
|
||
} else {
|
||
// Аккаунт изменен
|
||
address.value = accounts[0]
|
||
}
|
||
}
|
||
|
||
function handleChainChanged() {
|
||
// При смене сети перезагружаем страницу
|
||
window.location.reload()
|
||
}
|
||
|
||
// Обновляем watch
|
||
watch(isConnected, async (newValue) => {
|
||
try {
|
||
if (newValue) {
|
||
console.log('Кошелек подключен, адрес:', address.value)
|
||
|
||
if (!isInitialized.value) {
|
||
await initializeContract()
|
||
}
|
||
await checkNetwork()
|
||
} else {
|
||
console.log('Кошелек отключен')
|
||
currentPrice.value = null
|
||
contractOwner.value = null
|
||
isCorrectNetwork.value = false
|
||
error.value = ''
|
||
isInitialized.value = false
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка при обработке подключения:', err)
|
||
error.value = 'Ошибка при обработке подключения: ' + err.message
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// Обновляем функцию проверки сети
|
||
async function checkNetwork() {
|
||
try {
|
||
if (!walletProvider.value) {
|
||
isCorrectNetwork.value = false
|
||
error.value = 'Провайдер кошелька недоступен'
|
||
return
|
||
}
|
||
|
||
const ethersProvider = new BrowserProvider(walletProvider.value)
|
||
const network = await ethersProvider.getNetwork()
|
||
console.log('Текущая сеть:', network.chainId)
|
||
|
||
isCorrectNetwork.value = Number(network.chainId) === SEPOLIA_CHAIN_ID
|
||
|
||
if (!isCorrectNetwork.value) {
|
||
error.value = `Пожалуйста, переключитесь на сеть Sepolia (${SEPOLIA_CHAIN_ID}). Текущая сеть: ${network.chainId}`
|
||
} else {
|
||
error.value = ''
|
||
await Promise.all([fetchPrice(), fetchOwner()])
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка при проверке сети:', err)
|
||
isCorrectNetwork.value = false
|
||
error.value = 'Ошибка при проверке сети: ' + err.message
|
||
}
|
||
}
|
||
|
||
// Обновляем функцию fetchPrice с обработкой ошибок
|
||
async function fetchPrice() {
|
||
try {
|
||
const contract = new Contract(contractAddress, contractABI, provider)
|
||
const price = await contract.price()
|
||
currentPrice.value = price
|
||
console.log('Текущая цена:', formatEther(price), 'ETH')
|
||
} catch (err) {
|
||
console.error('Ошибка при получении цены:', err)
|
||
error.value = `Не удалось получить текущую цену: ${err.message}`
|
||
currentPrice.value = null
|
||
}
|
||
}
|
||
|
||
// Получение адреса владельца
|
||
async function fetchOwner() {
|
||
try {
|
||
const contract = new Contract(contractAddress, contractABI, provider)
|
||
contractOwner.value = await contract.owner()
|
||
} catch (err) {
|
||
console.error('Ошибка при получении адреса владельца:', err)
|
||
}
|
||
}
|
||
|
||
// Обновляем onMounted
|
||
onMounted(async () => {
|
||
console.log('Компонент смонтирован')
|
||
|
||
try {
|
||
await initializeContract()
|
||
|
||
if (provider) {
|
||
const contract = new Contract(contractAddress, contractABI, provider)
|
||
|
||
contract.on('Purchase', (buyer, amount) => {
|
||
console.log(`Новая покупка: ${amount} единиц от ${buyer}`)
|
||
fetchPrice()
|
||
})
|
||
|
||
return () => {
|
||
contract.removeAllListeners('Purchase')
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Ошибка при монтировании компонента:', err)
|
||
}
|
||
})
|
||
|
||
// Добавляем функцию для расчета общей стоимости
|
||
function calculateTotalCost() {
|
||
if (!currentPrice.value || !amount.value) return BigInt(0)
|
||
return currentPrice.value * BigInt(amount.value)
|
||
}
|
||
|
||
// Обновляем handlePurchase
|
||
async function handlePurchase() {
|
||
if (!amount.value) return
|
||
if (!isCorrectNetwork.value) {
|
||
error.value = 'Пожалуйста, переключитесь на сеть Sepolia'
|
||
return
|
||
}
|
||
|
||
error.value = ''
|
||
success.value = ''
|
||
|
||
try {
|
||
isLoading.value = true
|
||
const ethersProvider = new BrowserProvider(walletProvider.value)
|
||
const signer = await ethersProvider.getSigner()
|
||
const contract = new Contract(contractAddress, contractABI, signer)
|
||
|
||
const totalCost = calculateTotalCost()
|
||
console.log('Общая стоимость:', formatEther(totalCost), 'ETH')
|
||
|
||
const tx = await contract.purchase(amount.value, {
|
||
value: totalCost
|
||
})
|
||
console.log('Транзакция отправлена:', tx.hash)
|
||
await tx.wait()
|
||
console.log('Транзакция подтверждена')
|
||
|
||
amount.value = ''
|
||
success.value = 'Покупка успешно совершена!'
|
||
await fetchPrice()
|
||
} catch (err) {
|
||
console.error('Ошибка при покупке:', err)
|
||
if (err.message.includes('user rejected')) {
|
||
error.value = 'Транзакция отменена пользователем'
|
||
} else {
|
||
error.value = 'Произошла ошибка при совершении покупки: ' + err.message
|
||
}
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// Добавляем новые функции
|
||
async function handleSetPrice() {
|
||
if (!newPrice.value) return
|
||
error.value = ''
|
||
success.value = ''
|
||
|
||
try {
|
||
isLoading.value = true
|
||
const ethersProvider = new BrowserProvider(walletProvider.value)
|
||
const signer = await ethersProvider.getSigner()
|
||
const contract = new Contract(contractAddress, contractABI, signer)
|
||
|
||
const priceInWei = parseEther(newPrice.value.toString())
|
||
const tx = await contract.setPrice(priceInWei)
|
||
await tx.wait()
|
||
|
||
newPrice.value = ''
|
||
success.value = 'Цена успешно обновлена!'
|
||
await fetchPrice()
|
||
} catch (err) {
|
||
console.error('Ошибка при установке цены:', err)
|
||
error.value = 'Ошибка при установке цены: ' + err.message
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleWithdraw() {
|
||
error.value = ''
|
||
success.value = ''
|
||
|
||
try {
|
||
isLoading.value = true
|
||
const ethersProvider = new BrowserProvider(walletProvider.value)
|
||
const signer = await ethersProvider.getSigner()
|
||
const contract = new Contract(contractAddress, contractABI, signer)
|
||
|
||
const tx = await contract.withdraw()
|
||
await tx.wait()
|
||
|
||
success.value = 'Средства успешно выведены!'
|
||
} catch (err) {
|
||
console.error('Ошибка при выводе средств:', err)
|
||
error.value = 'Ошибка при выводе средств: ' + err.message
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.contract-interaction {
|
||
padding: 20px;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.wallet-info {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.contract-controls {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.amount-input {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.purchase-button {
|
||
padding: 8px 16px;
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.purchase-button:hover:not(:disabled) {
|
||
background-color: #45a049;
|
||
}
|
||
|
||
.purchase-button:disabled {
|
||
background-color: #cccccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.error-message {
|
||
color: #dc3545;
|
||
padding: 10px;
|
||
margin: 10px 0;
|
||
background-color: #f8d7da;
|
||
border: 1px solid #f5c6cb;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.price-info {
|
||
margin: 10px 0;
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.owner-controls {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 8px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.purchase-panel {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.admin-button {
|
||
padding: 8px 16px;
|
||
background-color: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.admin-button:hover:not(:disabled) {
|
||
background-color: #0056b3;
|
||
}
|
||
|
||
.withdraw-button {
|
||
margin-top: 10px;
|
||
background-color: #6c757d;
|
||
}
|
||
|
||
.withdraw-button:hover:not(:disabled) {
|
||
background-color: #5a6268;
|
||
}
|
||
|
||
.total-cost {
|
||
margin-top: 10px;
|
||
font-size: 0.9em;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.success-message {
|
||
color: #28a745;
|
||
margin-top: 10px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.loading-message {
|
||
color: #6c757d;
|
||
text-align: center;
|
||
padding: 20px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.auth-status {
|
||
margin: 10px 0;
|
||
padding: 10px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.authenticated {
|
||
color: #28a745;
|
||
}
|
||
|
||
.signout-button {
|
||
padding: 5px 10px;
|
||
background-color: #dc3545;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.signout-button:hover {
|
||
background-color: #c82333;
|
||
}
|
||
|
||
.connect-button {
|
||
padding: 10px 20px;
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.connect-button:hover {
|
||
background-color: #45a049;
|
||
}
|
||
</style> |