/** * Copyright (c) 2024-2025 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/VC-HB3-Accelerator */ import axios from 'axios'; import { ethers } from 'ethers'; import { SiweMessage } from 'siwe'; export const connectWallet = async () => { try { // console.log('Starting wallet connection...'); // Проверяем наличие MetaMask или другого Ethereum провайдера if (!window.ethereum) { // console.error('No Ethereum provider (like MetaMask) detected!'); return { success: false, error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.', }; } // console.log('MetaMask detected, requesting accounts...'); // Запрашиваем доступ к аккаунтам const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); // console.log('Got accounts:', accounts); if (!accounts || accounts.length === 0) { return { success: false, error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.', }; } // Берем первый аккаунт в списке const address = accounts[0]; // Нормализуем адрес (используем getAddress для совместимости) // Проверяем версию ethers - если v6, используем ethers.getAddress, иначе ethers.utils.getAddress const normalizedAddress = ethers.getAddress ? ethers.getAddress(address) : ethers.utils.getAddress(address); // console.log('Normalized address:', normalizedAddress); // Запрашиваем nonce с сервера // console.log('Requesting nonce...'); const nonceResponse = await axios.get(`/auth/nonce?address=${normalizedAddress}`); const nonce = nonceResponse.data.nonce; // console.log('Got nonce:', nonce); if (!nonce) { return { success: false, error: 'Не удалось получить nonce от сервера.', }; } // Для SIWE используем personal_sign напрямую через window.ethereum // Не используем ethers signer, так как он добавляет префикс, который нарушает SIWE формат // Получаем список документов для подписания let resources = [`${window.location.origin}/api/auth/verify`]; try { const docsResponse = await axios.get('/consent/documents'); if (docsResponse.data && docsResponse.data.length > 0) { docsResponse.data.forEach(doc => { resources.push(`${window.location.origin}/public/page/${doc.id}`); }); } } catch (error) { // Если не удалось получить документы, продолжаем без них console.warn('Не удалось получить список документов для подписания:', error); } // Создаем сообщение для подписи // Важно: domain должен быть hostname без протокола и порта (если порт стандартный) const domain = window.location.hostname === 'localhost' ? `localhost:${window.location.port}` : window.location.hostname; const origin = window.location.origin; // Создаем issuedAt один раз, чтобы использовать одинаковый в сообщении и запросе const issuedAt = new Date().toISOString(); // Создаем копию resources и сортируем (не мутируем исходный массив) const sortedResources = [...resources].sort(); // Создаем SIWE сообщение с документами в resources const message = new SiweMessage({ domain, address: normalizedAddress, statement: 'Sign in with Ethereum to the app.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.', uri: origin, version: '1', chainId: 1, // Ethereum mainnet nonce: nonce, issuedAt: issuedAt, resources: sortedResources, }); // Получаем строку сообщения для подписи const messageToSign = message.prepareMessage(); // Логируем для отладки console.log('🔐 [Frontend] Domain:', domain); console.log('🔐 [Frontend] Origin:', origin); console.log('🔐 [Frontend] Address:', normalizedAddress); console.log('🔐 [Frontend] Nonce:', nonce); console.log('🔐 [Frontend] IssuedAt:', issuedAt); console.log('🔐 [Frontend] Resources:', JSON.stringify(sortedResources)); console.log('🔐 [Frontend] SIWE message to sign:', messageToSign); console.log('🔐 [Frontend] Message length:', messageToSign.length); // Запрашиваем подпись через personal_sign (правильный способ для SIWE) // personal_sign подписывает сообщение С префиксом "\x19Ethereum Signed Message:\n" // ethers.verifyMessage() также добавляет этот префикс, поэтому они совместимы // Параметры: [message, address] - MetaMask принимает строку напрямую const signature = await window.ethereum.request({ method: 'personal_sign', params: [messageToSign, normalizedAddress.toLowerCase()], }); if (!signature) { return { success: false, error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.', }; } // console.log('Got signature:', signature); // Отправляем верификацию на сервер // console.log('Sending verification request...'); const requestData = { address: normalizedAddress, signature, nonce, issuedAt: issuedAt, // Используем тот же issuedAt, что и в сообщении }; // console.log('Request data:', requestData); const verifyResponse = await axios.post('/auth/verify', requestData, { withCredentials: true, }); // Обновляем интерфейс для отображения подключенного состояния document.body.classList.add('wallet-connected'); // Обновляем отображение адреса кошелька в UI const authDisplayEl = document.getElementById('auth-display'); if (authDisplayEl) { const shortAddress = `${normalizedAddress.substring(0, 6)}...${normalizedAddress.substring(normalizedAddress.length - 4)}`; authDisplayEl.innerHTML = `Кошелек: ${shortAddress}`; authDisplayEl.style.display = 'inline-block'; } // Скрываем кнопки авторизации и показываем кнопку выхода const authButtonsEl = document.getElementById('auth-buttons'); const logoutButtonEl = document.getElementById('logout-button'); if (authButtonsEl) authButtonsEl.style.display = 'none'; if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; // console.log('Verification response:', verifyResponse.data); if (verifyResponse.data.success) { return { success: true, address: normalizedAddress, userId: verifyResponse.data.userId, }; } else { return { success: false, error: verifyResponse.data.error || 'Ошибка верификации на сервере.', }; } } catch (error) { // console.error('Error connecting wallet:', error); // Формируем понятное сообщение об ошибке let errorMessage = 'Произошла ошибка при подключении кошелька.'; if (error.message && error.message.includes('MetaMask extension not found')) { errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.'; } else if (error.message && error.message.includes('Failed to connect to MetaMask')) { errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.'; } else if (error.code === 4001) { errorMessage = 'Вы отклонили запрос на подпись в MetaMask.'; } else if (error.message && error.message.includes('No accounts found')) { errorMessage = 'Аккаунты не найдены. Пожалуйста, разблокируйте MetaMask и попробуйте снова.'; } else if (error.message && error.message.includes('MetaMask not detected')) { errorMessage = 'MetaMask не обнаружен. Пожалуйста, установите расширение MetaMask.'; } else if (error.response && error.response.data && error.response.data.error) { errorMessage = error.response.data.error; } else if (error.message) { errorMessage = error.message; } return { success: false, error: errorMessage, }; } };