Initial commit

This commit is contained in:
2025-02-18 21:56:51 +03:00
commit b56e6b5e46
19 changed files with 6998 additions and 0 deletions

33
.gitignore vendored Normal file
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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 - Выйти из системы');
});

View 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

File diff suppressed because it is too large Load Diff

13
frontend/index.html Normal file
View 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
View 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
View 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>

View 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>

View 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>

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff