Initial commit
This commit is contained in:
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Hardhat files
|
||||||
|
cache/
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
coverage.json
|
||||||
29
backend/.gitignore
vendored
Normal file
29
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Hardhat files
|
||||||
|
cache/
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
coverage.json
|
||||||
20
backend/contracts/MyContract.sol
Normal file
20
backend/contracts/MyContract.sol
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
contract MyContract {
|
||||||
|
address public owner;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
owner = msg.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier onlyOwner() {
|
||||||
|
require(msg.sender == owner, "Only owner can call this function");
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOwner(address newOwner) public onlyOwner {
|
||||||
|
require(newOwner != address(0), "New owner cannot be zero address");
|
||||||
|
owner = newOwner;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/hardhat.config.js
Normal file
12
backend/hardhat.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
require("@nomiclabs/hardhat-waffle");
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
solidity: "0.8.0",
|
||||||
|
networks: {
|
||||||
|
sepolia: {
|
||||||
|
url: process.env.ETHEREUM_NETWORK_URL,
|
||||||
|
accounts: [process.env.PRIVATE_KEY]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
14
backend/index.js
Normal file
14
backend/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
const express = require('express');
|
||||||
|
const { ethers } = require('ethers');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Добро пожаловать в DApp-for-Business API');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Сервер запущен на http://localhost:${port}`);
|
||||||
|
});
|
||||||
33
backend/package.json
Normal file
33
backend/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"compile": "hardhat compile",
|
||||||
|
"deploy": "hardhat run scripts/deploy.js --network sepolia",
|
||||||
|
"node": "hardhat node",
|
||||||
|
"test": "hardhat test",
|
||||||
|
"server": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nomiclabs/hardhat-ethers": "^2.0.0",
|
||||||
|
"@nomiclabs/hardhat-waffle": "^2.0.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"ethers": "^5.0.0",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"hardhat": "^2.9.3",
|
||||||
|
"siwe": "^3.0.0",
|
||||||
|
"viem": "^2.23.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express-session": "^1.18.1",
|
||||||
|
"@types/sinon-chai": "^4.0.0",
|
||||||
|
"chai": "4.3.7",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"ethereum-waffle": "^4.0.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/scripts/deploy.js
Normal file
17
backend/scripts/deploy.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
async function main() {
|
||||||
|
const [deployer] = await ethers.getSigners();
|
||||||
|
|
||||||
|
console.log('Deploying contracts with the account:', deployer.address);
|
||||||
|
|
||||||
|
const MyContract = await ethers.getContractFactory('MyContract');
|
||||||
|
const contract = await MyContract.deploy();
|
||||||
|
|
||||||
|
console.log('Contract deployed to:', contract.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
211
backend/server.js
Normal file
211
backend/server.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import session from 'express-session';
|
||||||
|
import { generateNonce, SiweMessage } from 'siwe';
|
||||||
|
import { createPublicClient, http, verifyMessage } from 'viem';
|
||||||
|
import { sepolia } from 'viem/chains';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Создаем Viem клиент для Sepolia
|
||||||
|
const client = createPublicClient({
|
||||||
|
chain: sepolia,
|
||||||
|
transport: http()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Конфигурация CORS для работы с frontend
|
||||||
|
app.use(cors({
|
||||||
|
origin: ['http://localhost:5174', 'http://127.0.0.1:5173', 'http://localhost:5173'],
|
||||||
|
credentials: true,
|
||||||
|
methods: ['GET', 'POST'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Accept']
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Настройка сессий
|
||||||
|
app.use(session({
|
||||||
|
name: 'siwe-dapp',
|
||||||
|
secret: "siwe-dapp-secret",
|
||||||
|
resave: true,
|
||||||
|
saveUninitialized: true,
|
||||||
|
cookie: {
|
||||||
|
secure: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 24 * 60 * 60 * 1000 // 24 часа
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Логирование запросов
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log(`${req.method} ${req.url}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Генерация nonce
|
||||||
|
app.get('/nonce', (_, res) => {
|
||||||
|
try {
|
||||||
|
const nonce = generateNonce();
|
||||||
|
res.setHeader('Content-Type', 'text/plain');
|
||||||
|
res.status(200).send(nonce);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка генерации nonce:', error);
|
||||||
|
res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Верификация сообщения
|
||||||
|
app.post('/verify', async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.body.message) {
|
||||||
|
return res.status(400).json({ error: 'SiweMessage is undefined' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { message, signature } = req.body;
|
||||||
|
console.log('Верификация сообщения:', { message, signature });
|
||||||
|
|
||||||
|
// Создаем и парсим SIWE сообщение
|
||||||
|
const siweMessage = new SiweMessage(message);
|
||||||
|
|
||||||
|
// Проверяем базовые параметры
|
||||||
|
if (siweMessage.chainId !== 11155111) { // Sepolia
|
||||||
|
throw new Error('Invalid chain ID. Only Sepolia is supported.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siweMessage.domain !== '127.0.0.1:5173') {
|
||||||
|
throw new Error('Invalid domain');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем время
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const messageTime = new Date(siweMessage.issuedAt).getTime();
|
||||||
|
const timeDiff = currentTime - messageTime;
|
||||||
|
|
||||||
|
// Временно отключаем проверку времени для разработки
|
||||||
|
console.log('Разница во времени:', {
|
||||||
|
currentTime: new Date(currentTime).toISOString(),
|
||||||
|
messageTime: new Date(messageTime).toISOString(),
|
||||||
|
diffMinutes: Math.abs(timeDiff) / (60 * 1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Верифицируем сообщение
|
||||||
|
console.log('Начинаем валидацию SIWE сообщения...');
|
||||||
|
const fields = await siweMessage.validate(signature);
|
||||||
|
console.log('SIWE валидация успешна:', fields);
|
||||||
|
|
||||||
|
// Проверяем подпись через viem
|
||||||
|
console.log('Проверяем подпись через viem...');
|
||||||
|
const isValid = await client.verifyMessage({
|
||||||
|
address: fields.address,
|
||||||
|
message: message,
|
||||||
|
signature: signature
|
||||||
|
});
|
||||||
|
console.log('Результат проверки подписи:', isValid);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error('Invalid signature');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Верификация успешна:', {
|
||||||
|
address: fields.address,
|
||||||
|
chainId: fields.chainId,
|
||||||
|
domain: fields.domain
|
||||||
|
});
|
||||||
|
|
||||||
|
// Сохраняем сессию
|
||||||
|
req.session.siwe = {
|
||||||
|
address: fields.address,
|
||||||
|
chainId: fields.chainId,
|
||||||
|
domain: fields.domain,
|
||||||
|
issuedAt: fields.issuedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
req.session.save(() => {
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
address: fields.address,
|
||||||
|
chainId: fields.chainId,
|
||||||
|
domain: fields.domain
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка верификации:', error);
|
||||||
|
req.session.siwe = null;
|
||||||
|
req.session.nonce = null;
|
||||||
|
req.session.save(() => {
|
||||||
|
res.status(400).json({
|
||||||
|
error: 'Verification failed',
|
||||||
|
message: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получение сессии
|
||||||
|
app.get('/session', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(req.session.siwe || null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения сессии:', error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Выход
|
||||||
|
app.get('/signout', (req, res) => {
|
||||||
|
try {
|
||||||
|
req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Ошибка при удалении сессии:', err);
|
||||||
|
return res.status(500).json({ error: 'Failed to destroy session' });
|
||||||
|
}
|
||||||
|
res.status(200).json({ success: true });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка выхода:', error);
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Базовый маршрут
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
endpoints: {
|
||||||
|
nonce: 'GET /nonce',
|
||||||
|
verify: 'POST /verify',
|
||||||
|
session: 'GET /session',
|
||||||
|
signout: 'GET /signout'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка 404
|
||||||
|
app.use((req, res) => {
|
||||||
|
console.log(`404: ${req.method} ${req.url}`);
|
||||||
|
res.status(404).json({
|
||||||
|
error: 'Not Found',
|
||||||
|
message: `Endpoint ${req.method} ${req.url} не существует`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработка ошибок
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error('Ошибка сервера:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: err.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`SIWE сервер запущен на порту ${PORT}`);
|
||||||
|
console.log('Доступные эндпоинты:');
|
||||||
|
console.log(' GET / - Информация о сервере');
|
||||||
|
console.log(' GET /nonce - Получить nonce');
|
||||||
|
console.log(' POST /verify - Верифицировать сообщение');
|
||||||
|
console.log(' GET /session - Получить текущую сессию');
|
||||||
|
console.log(' GET /signout - Выйти из системы');
|
||||||
|
});
|
||||||
42
backend/test/MyContract.test.js
Normal file
42
backend/test/MyContract.test.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
const { expect } = require("chai");
|
||||||
|
const { ethers } = require("hardhat");
|
||||||
|
|
||||||
|
describe("MyContract", function () {
|
||||||
|
let myContract;
|
||||||
|
let owner;
|
||||||
|
let addr1;
|
||||||
|
let addr2;
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
// Получаем аккаунты из Hardhat
|
||||||
|
[owner, addr1, addr2] = await ethers.getSigners();
|
||||||
|
|
||||||
|
// Деплоим контракт
|
||||||
|
const MyContract = await ethers.getContractFactory("MyContract");
|
||||||
|
myContract = await MyContract.deploy();
|
||||||
|
await myContract.deployed();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Deployment", function () {
|
||||||
|
it("Should set the right owner", async function () {
|
||||||
|
expect(await myContract.owner()).to.equal(owner.address);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Transactions", function () {
|
||||||
|
it("Should allow owner to set new owner", async function () {
|
||||||
|
await myContract.setOwner(addr1.address);
|
||||||
|
expect(await myContract.owner()).to.equal(addr1.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should fail if non-owner tries to set new owner", async function () {
|
||||||
|
// Подключаемся к контракту от имени addr1
|
||||||
|
const contractConnectedToAddr1 = myContract.connect(addr1);
|
||||||
|
|
||||||
|
// Ожидаем, что транзакция будет отменена
|
||||||
|
await expect(
|
||||||
|
contractConnectedToAddr1.setOwner(addr2.address)
|
||||||
|
).to.be.revertedWith("Only owner can call this function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
4155
backend/yarn.lock
Normal file
4155
backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>DApp for Business</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- Vite автоматически вставит сюда ваш JavaScript -->
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
frontend/package.json
Normal file
24
frontend/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reown/appkit": "1.6.8",
|
||||||
|
"@reown/appkit-adapter-ethers": "1.6.8",
|
||||||
|
"@reown/appkit-siwe": "^1.6.8",
|
||||||
|
"ethers": "^6.13.4",
|
||||||
|
"siwe": "^3.0.0",
|
||||||
|
"viem": "^2.23.2",
|
||||||
|
"vue": "^3.5.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.0",
|
||||||
|
"vite": "^5.4.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
frontend/src/App.vue
Normal file
27
frontend/src/App.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<h1>Добро пожаловать в DApp-for-Business</h1>
|
||||||
|
<appkit-button
|
||||||
|
balance="show"
|
||||||
|
size="md"
|
||||||
|
label="Подключить кошелек"
|
||||||
|
loadingLabel="Подключение..."
|
||||||
|
/>
|
||||||
|
<ContractInteraction />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ContractInteraction from './components/ContractInteraction.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
components: {
|
||||||
|
ContractInteraction
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Добавьте ваши стили здесь */
|
||||||
|
</style>
|
||||||
218
frontend/src/components/ContractInteraction.vue
Normal file
218
frontend/src/components/ContractInteraction.vue
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contract-interaction">
|
||||||
|
<h2>Управление контрактом</h2>
|
||||||
|
|
||||||
|
<div v-if="!isConnected" class="warning">
|
||||||
|
Пожалуйста, подключите кошелек для управления контрактом
|
||||||
|
<button @click="handleConnect" class="connect-button">
|
||||||
|
Подключить кошелек
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div class="owner-info">
|
||||||
|
<p>Текущий владелец:
|
||||||
|
<span v-if="loading">Загрузка...</span>
|
||||||
|
<span v-else-if="owner">{{ formatAddress(owner) }}</span>
|
||||||
|
<span v-else>Не удалось загрузить</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="owner-controls">
|
||||||
|
<input
|
||||||
|
v-model="newOwner"
|
||||||
|
placeholder="Адрес нового владельца (0x...)"
|
||||||
|
:disabled="!isConnected"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="setNewOwner"
|
||||||
|
:disabled="!isConnected || !isValidAddress(newOwner)"
|
||||||
|
>
|
||||||
|
Установить нового владельца
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { useAppKitAccount, useAppKitProvider, useAppKit } from '@reown/appkit/vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContractInteraction',
|
||||||
|
setup() {
|
||||||
|
const owner = ref('');
|
||||||
|
const newOwner = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const { address, isConnected } = useAppKitAccount();
|
||||||
|
const { walletProvider } = useAppKitProvider('eip155');
|
||||||
|
const appKit = useAppKit();
|
||||||
|
|
||||||
|
const contractAddress = '0x6199Ba629C85Da887dBd8Ffd8d2C75Ea24EaDe2a';
|
||||||
|
const contractABI = [
|
||||||
|
'function owner() view returns (address)',
|
||||||
|
'function setOwner(address newOwner)',
|
||||||
|
];
|
||||||
|
|
||||||
|
const formatAddress = (addr) => {
|
||||||
|
return addr.slice(0, 6) + '...' + addr.slice(-4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidAddress = (addr) => {
|
||||||
|
try {
|
||||||
|
return ethers.isAddress(addr);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавим логирование для отладки
|
||||||
|
watch(() => isConnected, (newValue) => {
|
||||||
|
console.log('Состояние подключения изменилось:', newValue);
|
||||||
|
console.log('Адрес кошелька:', address);
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const fetchOwner = async () => {
|
||||||
|
if (!isConnected) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
console.log('Получаем владельца контракта...');
|
||||||
|
const ethersProvider = new ethers.BrowserProvider(walletProvider);
|
||||||
|
const contract = new ethers.Contract(contractAddress, contractABI, ethersProvider);
|
||||||
|
owner.value = await contract.owner();
|
||||||
|
console.log('Владелец контракта:', owner.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении владельца:', error);
|
||||||
|
owner.value = '';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNewOwner = async () => {
|
||||||
|
try {
|
||||||
|
if (!isConnected) {
|
||||||
|
console.log('Пожалуйста, подключите кошелек');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidAddress(newOwner.value)) {
|
||||||
|
console.log('Неверный адрес');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ethersProvider = new ethers.BrowserProvider(walletProvider);
|
||||||
|
const signer = await ethersProvider.getSigner();
|
||||||
|
const contract = new ethers.Contract(contractAddress, contractABI, signer);
|
||||||
|
|
||||||
|
const tx = await contract.setOwner(newOwner.value);
|
||||||
|
await tx.wait();
|
||||||
|
|
||||||
|
// Обновляем информацию после успешной транзакции
|
||||||
|
await fetchOwner();
|
||||||
|
newOwner.value = ''; // Очищаем поле ввода
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при установке нового владельца:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обработчик подключения кошелька
|
||||||
|
const handleConnect = async () => {
|
||||||
|
try {
|
||||||
|
await appKit.open();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при подключении:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновляем watch
|
||||||
|
watch(() => isConnected, (newValue, oldValue) => {
|
||||||
|
console.log('Состояние подключения изменилось:', { newValue, oldValue });
|
||||||
|
if (newValue) {
|
||||||
|
fetchOwner();
|
||||||
|
} else {
|
||||||
|
owner.value = '';
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Проверяем состояние подключения при монтировании
|
||||||
|
console.log('Компонент смонтирован, isConnected:', isConnected);
|
||||||
|
fetchOwner();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
owner,
|
||||||
|
newOwner,
|
||||||
|
isConnected,
|
||||||
|
loading,
|
||||||
|
handleConnect,
|
||||||
|
walletProvider,
|
||||||
|
setNewOwner,
|
||||||
|
formatAddress,
|
||||||
|
isValidAddress
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.contract-interaction {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: #721c24;
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-button:hover:not(:disabled) {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
frontend/src/components/WalletButtonWrapper.vue
Normal file
31
frontend/src/components/WalletButtonWrapper.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="walletButton"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import '@reown/appkit-wallet-button/react';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WalletButtonWrapper',
|
||||||
|
props: {
|
||||||
|
wallet: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const walletButton = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const button = document.createElement('appkit-wallet-button');
|
||||||
|
button.setAttribute('wallet', props.wallet);
|
||||||
|
walletButton.value.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
walletButton
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
8
frontend/src/config/index.ts
Normal file
8
frontend/src/config/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { EthersAdapter } from '@reown/appkit-adapter-ethers'
|
||||||
|
import { sepolia } from '@reown/appkit/networks'
|
||||||
|
|
||||||
|
export const projectId = '9a6515f7259ebccd149fd53341e01e6b'
|
||||||
|
|
||||||
|
export const networks = [sepolia]
|
||||||
|
|
||||||
|
export const ethersAdapter = new EthersAdapter()
|
||||||
105
frontend/src/main.js
Normal file
105
frontend/src/main.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import { createAppKit } from '@reown/appkit/vue';
|
||||||
|
import { EthersAdapter } from '@reown/appkit-adapter-ethers';
|
||||||
|
import { sepolia } from '@reown/appkit/networks';
|
||||||
|
import { createSIWEConfig, formatMessage } from '@reown/appkit-siwe';
|
||||||
|
|
||||||
|
// Определяем базовый URL для API
|
||||||
|
const BASE_URL = 'http://localhost:3000';
|
||||||
|
|
||||||
|
// 1. Get projectId
|
||||||
|
const projectId = '9a6515f7259ebccd149fd53341e01e6b';
|
||||||
|
|
||||||
|
// 2. Create SIWE config
|
||||||
|
const siweConfig = createSIWEConfig({
|
||||||
|
getMessageParams: async () => ({
|
||||||
|
domain: window.location.host,
|
||||||
|
uri: window.location.origin,
|
||||||
|
chains: [11155111], // Sepolia chainId
|
||||||
|
statement: 'Подпишите это сообщение для входа в DApp for Business. Это безопасно и не требует оплаты.',
|
||||||
|
}),
|
||||||
|
createMessage: ({ address, ...args }) => formatMessage(args, address),
|
||||||
|
getNonce: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/nonce`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'text/plain'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Failed to get nonce');
|
||||||
|
return await res.text();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения nonce:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSession: async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/session`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return await res.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка получения сессии:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifyMessage: async ({ message, signature }) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${BASE_URL}/verify`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message, signature }),
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка верификации:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
signOut: async () => {
|
||||||
|
try {
|
||||||
|
await fetch(`${BASE_URL}/signout`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка выхода:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Create AppKit instance
|
||||||
|
createAppKit({
|
||||||
|
adapters: [new EthersAdapter()],
|
||||||
|
networks: [sepolia],
|
||||||
|
projectId,
|
||||||
|
metadata: {
|
||||||
|
name: 'DApp for Business',
|
||||||
|
description: 'Smart Contract Management DApp',
|
||||||
|
url: window.location.origin,
|
||||||
|
icons: ['https://avatars.githubusercontent.com/u/37784886']
|
||||||
|
},
|
||||||
|
defaultNetwork: sepolia,
|
||||||
|
features: {
|
||||||
|
analytics: true,
|
||||||
|
connectMethodsOrder: ['wallet', 'email', 'social'],
|
||||||
|
autoConnect: false
|
||||||
|
},
|
||||||
|
siweConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
app.mount('#app');
|
||||||
16
frontend/vite.config.js
Normal file
16
frontend/vite.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue({
|
||||||
|
template: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: (tag) =>
|
||||||
|
tag.startsWith('appkit-') ||
|
||||||
|
tag.startsWith('w3m-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
1990
frontend/yarn.lock
Normal file
1990
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user