UNPKG

shogun-core

Version:

SHOGUN CORE - Core library for Shogun Ecosystem

535 lines (534 loc) 19.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Web3Connector = void 0; /** * The MetaMaskAuth class provides functionality for connecting, signing up, and logging in using MetaMask. */ const ethers_1 = require("ethers"); const errorHandler_1 = require("../../utils/errorHandler"); const eventEmitter_1 = require("../../utils/eventEmitter"); const derive_1 = __importDefault(require("../../gundb/derive")); /** * Class for MetaMask connection */ class Web3Connector extends eventEmitter_1.EventEmitter { MESSAGE_TO_SIGN = "I Love Shogun!"; DEFAULT_CONFIG = { cacheDuration: 30 * 60 * 1000, // 30 minutes maxRetries: 3, retryDelay: 1000, timeout: 60000, }; config; signatureCache = new Map(); provider = null; customProvider = null; customWallet = null; constructor(config = {}) { super(); this.config = { ...this.DEFAULT_CONFIG, ...config }; this.initProvider(); this.setupEventListeners(); } /** * Initialize the provider synchronously with fallback mechanisms * to handle conflicts between multiple wallet providers */ initProvider() { if (typeof window !== "undefined") { try { // Check if ethereum is available from any provider const ethereumProvider = this.getAvailableEthereumProvider(); if (ethereumProvider) { this.provider = new ethers_1.ethers.BrowserProvider(ethereumProvider); } else { console.warn("No compatible Ethereum provider found"); } } catch (error) { console.error("Failed to initialize BrowserProvider", error); } } else { console.warn("Window object not available (non-browser environment)"); } } /** * Get available Ethereum provider from multiple possible sources */ getAvailableEthereumProvider() { if (typeof window === "undefined") return undefined; // Define provider sources with priority order const providerSources = [ // Check if we have providers in the _ethereumProviders registry (from index.html) { source: () => window._ethereumProviders && window._ethereumProviders[0], name: "Registry Primary", }, { source: () => window.ethereum, name: "Standard ethereum" }, { source: () => window.web3?.currentProvider, name: "Legacy web3", }, { source: () => window.metamask, name: "MetaMask specific" }, { source: () => window.ethereum?.providers?.find((p) => p.isMetaMask), name: "MetaMask from providers array", }, { source: () => window.ethereum?.providers?.[0], name: "First provider in array", }, // Try known provider names { source: () => window.enkrypt?.providers?.ethereum, name: "Enkrypt", }, { source: () => window.coinbaseWalletExtension, name: "Coinbase", }, { source: () => window.trustWallet, name: "Trust Wallet" }, // Use special registry if available { source: () => Array.isArray(window._ethereumProviders) ? window._ethereumProviders.find((p) => !p._isProxy) : undefined, name: "Registry non-proxy", }, ]; // Try each provider source for (const { source, name } of providerSources) { try { const provider = source(); if (provider && typeof provider.request === "function") { return provider; } } catch (error) { // Continue to next provider source console.warn(`Error checking provider ${name}:`, error); continue; } } // No provider found console.warn("No compatible Ethereum provider found"); return undefined; } /** * Initialize the BrowserProvider (async method for explicit calls) */ async setupProvider() { try { if (typeof window !== "undefined") { // Check if ethereum is available from any provider const ethereumProvider = this.getAvailableEthereumProvider(); if (ethereumProvider) { this.provider = new ethers_1.ethers.BrowserProvider(ethereumProvider); } else { console.warn("No compatible Ethereum provider found"); } } else { console.warn("Window object not available (non-browser environment)"); } } catch (error) { console.error("Failed to initialize BrowserProvider", error); } } /** * Setup MetaMask event listeners using BrowserProvider */ setupEventListeners() { if (this.provider) { // Listen for network changes through ethers provider this.provider.on("network", (newNetwork, oldNetwork) => { this.emit("chainChanged", newNetwork); }); // Listen for account changes through the detected provider try { const ethereumProvider = this.getAvailableEthereumProvider(); if (ethereumProvider?.on) { ethereumProvider.on("accountsChanged", (accounts) => { this.emit("accountsChanged", accounts); }); // Also listen for chainChanged events directly ethereumProvider.on("chainChanged", (chainId) => { this.emit("chainChanged", { chainId }); }); } } catch (error) { console.warn("Failed to setup account change listeners", error); } } } /** * Cleanup event listeners */ cleanup() { if (this.provider) { this.provider.removeAllListeners(); } this.removeAllListeners(); } /** * Get cached signature if valid */ getCachedSignature(address) { const cached = this.signatureCache.get(address); if (!cached) return null; const now = Date.now(); if (now - cached.timestamp > this.config.cacheDuration) { this.signatureCache.delete(address); return null; } // Check for invalid/empty signature if (!cached.signature || typeof cached.signature !== "string" || cached.signature.length < 16) { console.warn(`Invalid cached signature for address ${address} (length: ${cached.signature ? cached.signature.length : 0}), deleting from cache.`); this.signatureCache.delete(address); return null; } return cached.signature; } /** * Cache signature */ cacheSignature(address, signature) { this.signatureCache.set(address, { signature, timestamp: Date.now(), address, }); } /** * Validates that the address is valid */ validateAddress(address) { if (!address) { throw new Error("Address not provided"); } try { const normalizedAddress = String(address).trim().toLowerCase(); if (!ethers_1.ethers.isAddress(normalizedAddress)) { throw new Error("Invalid address format"); } return ethers_1.ethers.getAddress(normalizedAddress); } catch (error) { errorHandler_1.ErrorHandler.handle(errorHandler_1.ErrorType.VALIDATION, "INVALID_ADDRESS", "Invalid Ethereum address provided", error); throw error; } } /** * Connects to MetaMask with retry logic using BrowserProvider */ async connectMetaMask() { try { if (!this.provider) { this.initProvider(); if (!this.provider) { throw new Error("MetaMask is not available. Please install MetaMask extension."); } } // First check if we can get the provider const ethereumProvider = this.getAvailableEthereumProvider(); if (!ethereumProvider) { throw new Error("No compatible Ethereum provider found"); } // Richiedi esplicitamente l'accesso all'account MetaMask let accounts = []; // Try multiple methods of requesting accounts for compatibility try { // Try the provider we found first accounts = await ethereumProvider.request({ method: "eth_requestAccounts", }); } catch (requestError) { console.warn("First account request failed, trying window.ethereum:", requestError); // Fallback to window.ethereum if available and different if (window.ethereum && window.ethereum !== ethereumProvider) { try { accounts = await window.ethereum.request({ method: "eth_requestAccounts", }); } catch (fallbackError) { console.error("All account request methods failed", fallbackError); throw new Error("User denied account access"); } } else { throw new Error("User denied account access"); } } if (!accounts || accounts.length === 0) { } for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) { try { const signer = await this.provider.getSigner(); const address = await signer.getAddress(); if (!address) { console.error("No address returned from signer"); throw new Error("No address returned from signer"); } this.emit("connected", { address }); return { success: true, address, }; } catch (error) { console.error(`Attempt ${attempt} failed:`, error); if (attempt === this.config.maxRetries) { throw error; } // Wait before retrying await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay)); } } throw new Error("Failed to get signer after all attempts"); } catch (error) { console.error("Failed to connect to MetaMask:", error); errorHandler_1.ErrorHandler.handle(errorHandler_1.ErrorType.WEBAUTHN, "METAMASK_CONNECTION_ERROR", error.message ?? "Unknown error while connecting to MetaMask", error); return { success: false, error: error.message }; } } /** * Generates credentials for the given address */ async generateCredentials(address) { try { const validAddress = this.validateAddress(address); // Check if we have a cached signature const cachedSignature = this.getCachedSignature(validAddress); if (cachedSignature) { return this.generateCredentialsFromSignature(validAddress, cachedSignature); } // Request signature with timeout let signature; try { signature = await this.requestSignatureWithTimeout(validAddress, this.MESSAGE_TO_SIGN, this.config.timeout); } catch (signingError) { // Gestione del fallimento di firma console.warn(`Failed to get signature: ${signingError}. Using fallback method.`); throw signingError; } // Cache the signature this.cacheSignature(validAddress, signature); return this.generateCredentialsFromSignature(validAddress, signature); } catch (error) { errorHandler_1.ErrorHandler.handle(errorHandler_1.ErrorType.WEBAUTHN, "CREDENTIALS_GENERATION_ERROR", error.message ?? "Error generating MetaMask credentials", error); throw error; } } /** * Generates credentials from a signature */ async generateCredentialsFromSignature(address, signature) { const hashedAddress = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(address)); const salt = `${address}_${signature}`; return await (0, derive_1.default)(hashedAddress, salt, { includeP256: true, }); } /** * Generates fallback credentials (for testing/development) */ generateFallbackCredentials(address) { console.warn("Using fallback credentials generation for address:", address); // Generate a deterministic but insecure fallback const fallbackSignature = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(address + "fallback")); return { username: address.toLowerCase(), password: fallbackSignature, message: this.MESSAGE_TO_SIGN, signature: fallbackSignature, }; } /** * Checks if MetaMask is available */ static isMetaMaskAvailable() { if (typeof window === "undefined") { return false; } // Check multiple possible sources const sources = [ () => window.ethereum, () => window.web3?.currentProvider, () => window.metamask, () => window._ethereumProviders?.[0], ]; for (const source of sources) { try { const provider = source(); if (provider && typeof provider.request === "function") { return true; } } catch { // Continue to next source } } return false; } /** * Requests signature with timeout */ requestSignatureWithTimeout(address, message, timeout = 30000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error("Signature request timed out")); }, timeout); const cleanup = () => { clearTimeout(timeoutId); }; const errorHandler = (error) => { cleanup(); reject(error); }; const initializeAndSign = async () => { try { const signer = await this.provider.getSigner(); const signerAddress = await signer.getAddress(); // Verify the signer address matches the expected address if (signerAddress.toLowerCase() !== address.toLowerCase()) { throw new Error(`Signer address (${signerAddress}) does not match expected address (${address})`); } const signature = await signer.signMessage(message); cleanup(); resolve(signature); } catch (error) { console.error("Failed to request signature:", error); errorHandler(error); } }; initializeAndSign(); }); } /** * Checks if the connector is available */ isAvailable() { return Web3Connector.isMetaMaskAvailable(); } /** * Sets a custom provider for testing/development */ setCustomProvider(rpcUrl, privateKey) { try { this.customProvider = new ethers_1.ethers.JsonRpcProvider(rpcUrl); this.customWallet = new ethers_1.ethers.Wallet(privateKey, this.customProvider); } catch (error) { throw new Error(`Error configuring provider: ${error.message ?? "Unknown error"}`); } } /** * Get active signer instance using BrowserProvider */ async getSigner() { try { if (this.customWallet) { return this.customWallet; } if (!this.provider) { this.initProvider(); } if (!this.provider) { throw new Error("Provider not initialized"); } return await this.provider.getSigner(); } catch (error) { throw new Error(`Unable to get Ethereum signer: ${error.message || "Unknown error"}`); } } /** * Get active provider instance using BrowserProvider */ async getProvider() { if (this.customProvider) { return this.customProvider; } if (!this.provider) { this.initProvider(); } return this.provider; } /** * Generate deterministic password from signature * @param signature - Cryptographic signature * @returns 64-character hex string * @throws {Error} For invalid signature */ async generatePassword(signature) { if (!signature) { throw new Error("Invalid signature"); } const hash = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(signature)); return hash.slice(2, 66); // Remove 0x and use first 32 bytes } /** * Verify message signature * @param message - Original signed message * @param signature - Cryptographic signature * @returns Recovered Ethereum address * @throws {Error} For invalid inputs */ async verifySignature(message, signature) { if (!message || !signature) { throw new Error("Invalid message or signature"); } try { return ethers_1.ethers.verifyMessage(message, signature); } catch (error) { throw new Error("Invalid message or signature"); } } /** * Get browser-based Ethereum signer * @returns Browser provider signer * @throws {Error} If MetaMask not detected */ async getEthereumSigner() { if (!Web3Connector.isMetaMaskAvailable()) { throw new Error("MetaMask not found. Please install MetaMask to continue."); } try { const ethereum = window.ethereum; await ethereum.request({ method: "eth_requestAccounts", }); const provider = new ethers_1.ethers.BrowserProvider(ethereum); return provider.getSigner(); } catch (error) { throw new Error(`Error accessing MetaMask: ${error.message ?? "Unknown error"}`); } } } exports.Web3Connector = Web3Connector; if (typeof window !== "undefined") { window.Web3Connector = Web3Connector; } else if (typeof global !== "undefined") { global.Web3Connector = Web3Connector; }