UNPKG

@jim4565/dapp-wrapper-react

Version:

React components and hooks for @jim4565/dapp-wrapper with persistent wallet connection

1,418 lines (1,410 loc) 79.8 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import React, { createContext, useReducer, useRef, useCallback, useEffect, useMemo, useState } from 'react'; // Error Types var WalletErrorCode; (function (WalletErrorCode) { WalletErrorCode[WalletErrorCode["USER_REJECTED"] = 4001] = "USER_REJECTED"; WalletErrorCode[WalletErrorCode["UNAUTHORIZED"] = 4100] = "UNAUTHORIZED"; WalletErrorCode[WalletErrorCode["UNSUPPORTED_METHOD"] = 4200] = "UNSUPPORTED_METHOD"; WalletErrorCode[WalletErrorCode["DISCONNECTED"] = 4900] = "DISCONNECTED"; WalletErrorCode[WalletErrorCode["CHAIN_DISCONNECTED"] = 4901] = "CHAIN_DISCONNECTED"; WalletErrorCode[WalletErrorCode["UNKNOWN_ERROR"] = -1] = "UNKNOWN_ERROR"; })(WalletErrorCode || (WalletErrorCode = {})); class WalletError extends Error { constructor(message, options) { super(message); this.name = 'WalletError'; this.code = (options === null || options === void 0 ? void 0 : options.code) || WalletErrorCode.UNKNOWN_ERROR; this.reason = options === null || options === void 0 ? void 0 : options.reason; this.method = options === null || options === void 0 ? void 0 : options.method; if ((options === null || options === void 0 ? void 0 : options.cause) && 'cause' in Error.prototype) { this.cause = options.cause; } } } // Storage Keys (consistent naming) const STORAGE_KEYS = { WALLET_ADDRESS: 'incentiv_wallet_address', WALLET_CONNECTED: 'incentiv_wallet_connected', EVER_CONNECTED: 'incentiv_wallet_ever_connected', LAST_CONNECTED_AT: 'incentiv_wallet_last_connected', CONTRACTS: 'incentiv_wallet_contracts' }; /** * WICHTIGE ÄNDERUNG in @jim4565/dapp-wrapper v1.x: * * Die zugrunde liegende Bibliothek unterscheidet jetzt automatisch zwischen: * * 1. VIEW/PURE Funktionen: * - Lesen nur Blockchain-Daten * - Benötigen KEINE Wallet-Verbindung * - Werden direkt mit dem Provider ausgeführt * - Beispiele: balanceOf, totalSupply, getName * * 2. NONPAYABLE/PAYABLE Funktionen: * - Ändern den Blockchain-State * - Benötigen eine Wallet-Verbindung für die Signatur * - Werden mit dem Signer ausgeführt * - Beispiele: transfer, approve, mint * * Diese Unterscheidung passiert automatisch basierend auf der ABI stateMutability. * * Für React-Entwickler bedeutet das: * - useContractRead: Funktioniert ohne Wallet-Verbindung * - useContractWrite: Erfordert weiterhin eine Wallet-Verbindung * - useContract: Stellt Contract immer zur Verfügung, Wallet-Check erfolgt bei Methodenaufruf */ /** * PersistenceService - MetaMask-style LocalStorage Management * * Handles secure storage and retrieval of wallet connection data * with proper error handling and data validation. */ class PersistenceService { /** * Save wallet connection data securely */ static saveWalletData(address) { try { const timestamp = Date.now(); localStorage.setItem(STORAGE_KEYS.WALLET_ADDRESS, address); localStorage.setItem(STORAGE_KEYS.WALLET_CONNECTED, 'true'); localStorage.setItem(STORAGE_KEYS.EVER_CONNECTED, 'true'); localStorage.setItem(STORAGE_KEYS.LAST_CONNECTED_AT, timestamp.toString()); } catch (error) { console.warn('⚠️ Failed to save wallet data:', error); throw new WalletError('Failed to persist wallet connection', { code: WalletErrorCode.UNKNOWN_ERROR, cause: error }); } } /** * Load stored wallet connection data */ static loadWalletData() { try { const address = localStorage.getItem(STORAGE_KEYS.WALLET_ADDRESS); const wasConnected = localStorage.getItem(STORAGE_KEYS.WALLET_CONNECTED) === 'true'; const hasEverConnected = localStorage.getItem(STORAGE_KEYS.EVER_CONNECTED) === 'true'; const lastConnectedAt = parseInt(localStorage.getItem(STORAGE_KEYS.LAST_CONNECTED_AT) || '0', 10); // Validate data integrity if (address && !this.isValidAddress(address)) { console.warn('⚠️ Invalid address in storage, clearing data'); this.clearWalletData(); return this.getEmptyWalletData(); } // Check if data is too old (older than 30 days) const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); if (lastConnectedAt > 0 && lastConnectedAt < thirtyDaysAgo) { this.clearWalletData(); return this.getEmptyWalletData(); } return { address, wasConnected, hasEverConnected, lastConnectedAt }; } catch (error) { console.warn('⚠️ Failed to load wallet data:', error); return this.getEmptyWalletData(); } } /** * Clear all wallet-related data */ static clearWalletData() { try { const keysToRemove = Object.values(STORAGE_KEYS); keysToRemove.forEach(key => { localStorage.removeItem(key); }); } catch (error) { console.warn('⚠️ Failed to clear wallet data:', error); } } /** * Save contract configurations */ static saveContracts(contracts) { try { const contractData = { version: this.VERSION, timestamp: Date.now(), contracts: contracts.map(contract => ({ name: contract.name, address: contract.address, // Don't store full ABI for security, just basic info hasABI: !!contract.abi })) }; localStorage.setItem(STORAGE_KEYS.CONTRACTS, JSON.stringify(contractData)); } catch (error) { console.warn('⚠️ Failed to save contract data:', error); } } /** * Load contract configurations */ static loadContracts() { try { const stored = localStorage.getItem(STORAGE_KEYS.CONTRACTS); if (!stored) return []; const data = JSON.parse(stored); // Validate version compatibility if (data.version !== this.VERSION) { localStorage.removeItem(STORAGE_KEYS.CONTRACTS); return []; } return data.contracts || []; } catch (error) { console.warn('⚠️ Failed to load contract data:', error); return []; } } /** * Check if wallet should auto-reconnect * * Returns true only if: * 1. User has ever connected before (prevents popup for first-time users) * 2. Was connected in last session * 3. Has valid stored address * 4. Connection is not too old */ static shouldAutoReconnect() { const { address, wasConnected, hasEverConnected, lastConnectedAt } = this.loadWalletData(); if (!hasEverConnected) { return false; } if (!address || !wasConnected) { return false; } // Check if connection is recent (within last 7 days) const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); if (lastConnectedAt > 0 && lastConnectedAt < sevenDaysAgo) { return false; } return true; } /** * Get storage usage statistics */ static getStorageStats() { const items = []; let totalSize = 0; Object.values(STORAGE_KEYS).forEach(key => { const value = localStorage.getItem(key); if (value !== null) { const size = new Blob([value]).size; totalSize += size; items.push({ key, value: value.substring(0, 100), // Truncate for display size }); } }); return { totalItems: items.length, totalSize, items }; } /** * Validate if address format is correct */ static isValidAddress(address) { return /^0x[a-fA-F0-9]{40}$/.test(address); } /** * Get empty wallet data structure */ static getEmptyWalletData() { return { address: null, wasConnected: false, hasEverConnected: false, lastConnectedAt: 0 }; } /** * Check if localStorage is available */ static isStorageAvailable() { try { const testKey = `${this.STORAGE_PREFIX}test`; localStorage.setItem(testKey, 'test'); localStorage.removeItem(testKey); return true; } catch (_a) { return false; } } /** * Migrate data from old storage format (if needed) */ static migrateStorageIfNeeded() { try { // Check for old format data and migrate if needed const oldAddress = localStorage.getItem('wallet_address'); const oldConnected = localStorage.getItem('wallet_connected'); if (oldAddress && !localStorage.getItem(STORAGE_KEYS.WALLET_ADDRESS)) { localStorage.setItem(STORAGE_KEYS.WALLET_ADDRESS, oldAddress); localStorage.setItem(STORAGE_KEYS.WALLET_CONNECTED, oldConnected || 'false'); localStorage.setItem(STORAGE_KEYS.EVER_CONNECTED, 'true'); localStorage.setItem(STORAGE_KEYS.LAST_CONNECTED_AT, Date.now().toString()); // Remove old keys localStorage.removeItem('wallet_address'); localStorage.removeItem('wallet_connected'); } } catch (error) { console.warn('⚠️ Storage migration failed:', error); } } } PersistenceService.VERSION = '1.0.0'; PersistenceService.STORAGE_PREFIX = 'incentiv_wallet_'; // WalletError is now imported from types /** * IncentivWalletService - Professional wrapper around @jim4565/dapp-wrapper * * Provides MetaMask-style connection patterns and 3-tier reconnection strategy * for seamless integration with React components. */ class IncentivWalletService { constructor() { this.isInitialized = false; this.eventListeners = new Map(); // Note: IncentivWrapper will be injected or imported // For now, we prepare the interface this.initializeEventHandlers(); } /** * Initialize with IncentivWrapper instance */ async initialize(wrapperInstance) { try { if (wrapperInstance) { this.wrapper = wrapperInstance; } else { // Dynamic import of @jim4565/dapp-wrapper const { IncentivWrapper } = await import('@jim4565/dapp-wrapper'); this.wrapper = new IncentivWrapper(); } this.isInitialized = true; } catch (error) { console.error('❌ Failed to initialize IncentivWalletService:', error); throw new WalletError('Failed to initialize wallet service', { code: WalletErrorCode.UNKNOWN_ERROR, cause: error }); } } /** * 3-TIER RECONNECTION STRATEGY * Following MetaMask and wagmi best practices */ /** * TIER 1: Silent Connection Check * Check if SDK already has an active session without triggering any popups */ async checkSilentConnection() { var _a, _b; if (!this.isInitialized) { return null; } try { // Check if wrapper already has an active connection if (this.wrapper.isConnected && this.wrapper.isConnected()) { const address = await this.extractAddressFromWrapper(); const normalizedAddress = this.normalizeAndValidateAddress(address); if (normalizedAddress) { return normalizedAddress; } } // Alternative: Check if connection state is available const connectionState = (_b = (_a = this.wrapper).getConnectionState) === null || _b === void 0 ? void 0 : _b.call(_a); if ((connectionState === null || connectionState === void 0 ? void 0 : connectionState.isConnected) && connectionState.address) { const normalizedStateAddress = this.normalizeAndValidateAddress(connectionState.address); if (normalizedStateAddress) { return normalizedStateAddress; } } return null; } catch (error) { return null; } } /** * TIER 2: Session Restore * Try to restore previous session without user interaction */ async tryRestoreSession(expectedAddress) { if (!this.isInitialized) { return false; } try { // Method 1: Try to restore connection directly if (this.wrapper.restoreConnection) { const restored = await this.wrapper.restoreConnection(expectedAddress); if (restored) { return true; } } // Method 2: Try to create signer without popup if (this.wrapper.getSigner) { try { const signer = await this.wrapper.getSigner(); const signerAddress = await signer.getAddress(); if ((signerAddress === null || signerAddress === void 0 ? void 0 : signerAddress.toLowerCase()) === expectedAddress.toLowerCase()) { return true; } } catch (signerError) { // Silent failure - this is expected if no session exists } } // Method 3: Check if connection is implicitly restored if (this.wrapper.userAddress && this.wrapper.userAddress.toLowerCase() === expectedAddress.toLowerCase()) { return true; } return false; } catch (error) { return false; } } /** * TIER 3: Full Connect * Complete connection flow with user interaction (popup) */ async connect() { if (!this.isInitialized) { throw new WalletError('Wallet service not initialized'); } try { // Use the wrapper's connect method await this.wrapper.connect(); // Multiple attempts to get the address from wrapper with different property names const rawAddress = await this.extractAddressFromWrapper(); // Normalize and validate address with detailed error reporting const address = this.normalizeAndValidateAddress(rawAddress); if (!address) { console.error('Address validation failed:', { received: rawAddress, type: typeof rawAddress, isArray: Array.isArray(rawAddress), wrapperProperties: this.debugWrapperState() }); throw new WalletError(`Invalid address received from wallet: ${JSON.stringify(rawAddress)}`); } // Save connection data for future reconnections PersistenceService.saveWalletData(address); // Emit connection event this.emit('connect', { address }); return address; } catch (error) { console.error('❌ Wallet connection failed:', error); // Handle specific error cases const errorWithCode = error; if (errorWithCode.code === 4001) { throw new WalletError('User rejected connection request', { code: WalletErrorCode.USER_REJECTED, method: 'connect' }); } if (errorWithCode.code === -32002) { throw new WalletError('Connection request already pending', { code: WalletErrorCode.UNKNOWN_ERROR, method: 'connect' }); } throw new WalletError('Failed to connect wallet', { code: WalletErrorCode.UNKNOWN_ERROR, cause: error, method: 'connect' }); } } /** * Get wallet balance with multiple fallback strategies */ async getBalance(address) { var _a; if (!this.isInitialized) { throw new WalletError('Wallet service not initialized'); } try { // Get target address using robust extraction let targetAddress = address; if (!targetAddress) { targetAddress = await this.extractAddressFromWrapper(); const normalizedAddress = this.normalizeAndValidateAddress(targetAddress); if (!normalizedAddress) { throw new WalletError('No valid address available for balance query'); } targetAddress = normalizedAddress; } // Strategy 1: Use wrapper's native balance method if (this.wrapper.getBalance && typeof this.wrapper.getBalance === 'function') { try { const balance = await this.wrapper.getBalance(targetAddress); return balance; } catch (error) { } } // Strategy 2: Use wrapper's getWalletBalance method if (this.wrapper.getWalletBalance && typeof this.wrapper.getWalletBalance === 'function') { try { const balance = await this.wrapper.getWalletBalance(targetAddress); return balance; } catch (error) { } } // Strategy 3: Use provider directly (most common fallback) if (this.wrapper.provider) { try { const balanceWei = await this.wrapper.provider.getBalance(targetAddress); // Convert Wei to Ether using ethers.js let balanceEth; if (typeof balanceWei === 'string' || typeof balanceWei === 'number') { // Simple conversion for basic numbers const weiNumber = BigInt(balanceWei.toString()); const ethNumber = Number(weiNumber) / 1e18; balanceEth = ethNumber.toString(); } else if (balanceWei.toString) { // Handle BigNumber or other objects with toString const weiString = balanceWei.toString(); const weiNumber = BigInt(weiString); const ethNumber = Number(weiNumber) / 1e18; balanceEth = ethNumber.toFixed(18).replace(/\.?0+$/, ''); // Remove trailing zeros } else { // Fallback: return raw value as string balanceEth = balanceWei.toString(); } return balanceEth; } catch (error) { } } // Strategy 4: Use signer provider if ((_a = this.wrapper.signer) === null || _a === void 0 ? void 0 : _a.provider) { try { const balanceWei = await this.wrapper.signer.provider.getBalance(targetAddress); const weiNumber = BigInt(balanceWei.toString()); const ethNumber = Number(weiNumber) / 1e18; const balanceEth = ethNumber.toFixed(18).replace(/\.?0+$/, ''); return balanceEth; } catch (error) { } } // Strategy 5: Check if ethers is available globally for direct RPC call try { const ethers = await import('ethers'); const provider = new ethers.providers.JsonRpcProvider('https://rpc.staging.incentiv.net'); const balanceWei = await provider.getBalance(targetAddress); const balanceEth = ethers.utils.formatEther(balanceWei); return balanceEth; } catch (error) { } throw new WalletError('No balance method available after trying all strategies'); } catch (error) { console.error('❌ Balance fetch failed:', error); throw new WalletError('Failed to fetch balance', { code: WalletErrorCode.UNKNOWN_ERROR, cause: error, method: 'getBalance' }); } } /** * Get contract instance */ getContract(contractName) { if (!this.isInitialized) { throw new WalletError('Wallet service not initialized'); } try { // Use wrapper's getContract method const contract = this.wrapper.getContract(contractName); if (!contract) { console.warn(`⚠️ Contract '${contractName}' not found`); return null; } return contract; } catch (error) { console.error(`❌ Failed to get contract '${contractName}':`, error); throw new WalletError(`Failed to get contract: ${contractName}`, { code: WalletErrorCode.UNKNOWN_ERROR, cause: error, method: 'getContract' }); } } /** * Register contracts with the wrapper */ registerContracts(contracts) { if (!this.isInitialized) { throw new WalletError('Wallet service not initialized'); } try { if (this.wrapper.registerContracts) { this.wrapper.registerContracts(contracts); } else { // Fallback to individual registration contracts.forEach(contract => { if (this.wrapper.registerContract) { this.wrapper.registerContract(contract.name, contract.address, contract.abi); } }); } // Save contract info to persistence PersistenceService.saveContracts(contracts); } catch (error) { console.error('❌ Contract registration failed:', error); throw new WalletError('Failed to register contracts', { code: WalletErrorCode.UNKNOWN_ERROR, cause: error, method: 'registerContracts' }); } } /** * Send transaction through wrapper */ async sendTransaction(txRequest) { if (!this.isInitialized) { throw new WalletError('Wallet service not initialized'); } try { // @jim4565/dapp-wrapper gibt ein Objekt mit { hash, receipt } zurück const result = await this.wrapper.sendTransaction(txRequest); // Emit transaction event this.emit('transaction', { hash: result.hash, status: 'confirmed', // Status ist 'confirmed' da wir das Receipt haben receipt: result.receipt, data: result }); return result; } catch (error) { console.error('❌ Transaction failed:', error); throw new WalletError('Transaction failed', { code: WalletErrorCode.UNKNOWN_ERROR, cause: error, method: 'sendTransaction' }); } } /** * Disconnect wallet */ disconnect() { var _a; try { if ((_a = this.wrapper) === null || _a === void 0 ? void 0 : _a.disconnect) { this.wrapper.disconnect(); } // Clear persistence PersistenceService.clearWalletData(); // Emit disconnect event this.emit('disconnect', {}); } catch (error) { console.warn('⚠️ Disconnect error:', error); } } /** * Check if wallet is connected */ isConnected() { var _a, _b, _c; if (!this.isInitialized) return false; return !!(((_b = (_a = this.wrapper) === null || _a === void 0 ? void 0 : _a.isConnected) === null || _b === void 0 ? void 0 : _b.call(_a)) || ((_c = this.wrapper) === null || _c === void 0 ? void 0 : _c.userAddress)); } /** * Get current user address */ async getUserAddress() { if (!this.isInitialized) return null; try { const address = await this.extractAddressFromWrapper(); return this.normalizeAndValidateAddress(address); } catch (error) { console.error('Error getting user address:', error); return null; } } /** * Event handling (MetaMask-style) */ on(event, callback) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(callback); } off(event, callback) { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } emit(event, data) { const listeners = this.eventListeners.get(event) || []; listeners.forEach(callback => { try { callback(data); } catch (error) { console.error(`Error in event listener for '${event}':`, error); } }); } /** * Initialize event handlers for wrapper events */ initializeEventHandlers() { // Will be populated after wrapper initialization // This sets up listeners for wrapper events like accountsChanged, disconnect, etc. } /** * Extract address from wrapper with multiple fallback strategies */ async extractAddressFromWrapper() { if (!this.wrapper) { return null; } try { // Strategy 1: Standard userAddress property if (this.wrapper.userAddress) { return this.wrapper.userAddress; } // Strategy 2: Alternative address properties const addressProperties = ['address', 'connectedAddress', 'walletAddress', 'account']; for (const prop of addressProperties) { if (this.wrapper[prop]) { return this.wrapper[prop]; } } // Strategy 3: Call address getter methods const addressMethods = ['getAddress', 'getUserAddress', 'getConnectedAddress', 'getAccount']; for (const method of addressMethods) { if (typeof this.wrapper[method] === 'function') { try { const result = await this.wrapper[method](); if (result) { return result; } } catch (error) { } } } // Strategy 4: Check signer address if (this.wrapper.signer) { try { const signerAddress = await this.wrapper.signer.getAddress(); if (signerAddress) { return signerAddress; } } catch (error) { } } // Strategy 5: Check provider accounts if (this.wrapper.provider) { try { const accounts = await this.wrapper.provider.listAccounts(); if (accounts && accounts.length > 0) { return accounts[0]; } } catch (error) { } } console.warn('⚠️ No address found in wrapper after all strategies'); return null; } catch (error) { console.error('❌ Error extracting address from wrapper:', error); return null; } } /** * Debug wrapper state for troubleshooting */ debugWrapperState() { if (!this.wrapper) { return { wrapper: 'null' }; } const state = { hasWrapper: !!this.wrapper, wrapperType: typeof this.wrapper }; // Check common properties const propsToCheck = [ 'userAddress', 'address', 'connectedAddress', 'walletAddress', 'account', 'isConnected', 'connected', 'signer', 'provider' ]; propsToCheck.forEach(prop => { if (prop in this.wrapper) { state[prop] = this.wrapper[prop]; } }); // Check available methods const methodsToCheck = [ 'getAddress', 'getUserAddress', 'getConnectedAddress', 'getAccount', 'connect', 'disconnect', 'isConnected' ]; state.availableMethods = methodsToCheck.filter(method => typeof this.wrapper[method] === 'function'); return state; } /** * Normalize and validate Ethereum address with flexible handling * Handles various formats that MetaMask/wallets might return */ normalizeAndValidateAddress(address) { try { // Handle null/undefined if (!address) { return null; } // Handle array responses (MetaMask eth_requestAccounts returns ['0x...']) let normalizedAddress; if (Array.isArray(address)) { if (address.length === 0) { return null; } normalizedAddress = address[0]; } else if (typeof address === 'string') { normalizedAddress = address; } else { console.warn('Unexpected address type:', typeof address, address); return null; } // Trim whitespace and ensure string normalizedAddress = normalizedAddress.trim(); // Basic format validation if (!normalizedAddress || normalizedAddress.length !== 42) { return null; } // Must start with 0x if (!normalizedAddress.startsWith('0x')) { return null; } // Case-insensitive hex validation (accepts checksum and non-checksum) const hexPart = normalizedAddress.slice(2); if (!/^[a-fA-F0-9]{40}$/.test(hexPart)) { return null; } // Return in lowercase for consistent handling return normalizedAddress.toLowerCase(); } catch (error) { console.error('Address normalization error:', error); return null; } } /** * Legacy method for backward compatibility */ isValidAddress(address) { return this.normalizeAndValidateAddress(address) !== null; } /** * Cleanup resources */ destroy() { this.eventListeners.clear(); this.isInitialized = false; } } var IncentivWalletService$1 = /*#__PURE__*/Object.freeze({ __proto__: null, IncentivWalletService: IncentivWalletService }); /** * Initial Wallet State */ const initialWalletState = { address: undefined, isConnected: false, isConnecting: false, isReconnecting: false, isDisconnecting: false, error: null }; /** * Wallet State Reducer * * Follows Redux patterns for predictable state management * with proper error handling and loading states. */ function walletReducer(state, action) { var _a, _b; switch (action.type) { case 'SET_CONNECTING': return { ...state, isConnecting: true, isReconnecting: false, isDisconnecting: false, error: null }; case 'SET_RECONNECTING': return { ...state, isConnecting: false, isReconnecting: true, isDisconnecting: false, error: null }; case 'SET_CONNECTED': return { ...state, address: action.payload.address, isConnected: true, isConnecting: false, isReconnecting: false, isDisconnecting: false, error: null }; case 'SET_DISCONNECTING': return { ...state, isDisconnecting: true, isConnecting: false, isReconnecting: false, error: null }; case 'SET_DISCONNECTED': return { ...initialWalletState, // Reset to initial state // Keep some state for better UX error: ((_a = action.payload) === null || _a === void 0 ? void 0 : _a.error) || null }; case 'SET_ERROR': return { ...state, isConnecting: false, isReconnecting: false, isDisconnecting: false, error: action.payload.error }; case 'CLEAR_ERROR': return { ...state, error: null }; case 'RESET_STATE': return { ...initialWalletState, error: ((_b = action.payload) === null || _b === void 0 ? void 0 : _b.keepError) ? state.error : null }; default: console.warn('Unknown wallet action type:', action.type); return state; } } /** * Action Creators - Type-safe action creation */ const walletActions = { setConnecting: () => ({ type: 'SET_CONNECTING' }), setReconnecting: () => ({ type: 'SET_RECONNECTING' }), setConnected: (address) => ({ type: 'SET_CONNECTED', payload: { address } }), setDisconnecting: () => ({ type: 'SET_DISCONNECTING' }), setDisconnected: (error) => ({ type: 'SET_DISCONNECTED', payload: error ? { error } : undefined }), setError: (error) => ({ type: 'SET_ERROR', payload: { error } }), clearError: () => ({ type: 'CLEAR_ERROR' }), resetState: (keepError) => ({ type: 'RESET_STATE', payload: keepError ? { keepError } : undefined }) }; /** * Selector functions for derived state */ const walletSelectors = { /** * Check if any loading state is active */ isLoading: (state) => { return state.isConnecting || state.isReconnecting || state.isDisconnecting; }, /** * Check if wallet is ready for transactions */ isReady: (state) => { return state.isConnected && !!state.address && !walletSelectors.isLoading(state); }, /** * Get formatted address for display */ getDisplayAddress: (state) => { if (!state.address) return ''; return `${state.address.slice(0, 6)}...${state.address.slice(-4)}`; }, /** * Check if error should be displayed to user */ shouldShowError: (state) => { if (!state.error) return false; // Don't show user rejection errors if ('code' in state.error && state.error.code === WalletErrorCode.USER_REJECTED) { return false; } return true; }, /** * Get user-friendly error message */ getErrorMessage: (state) => { if (!state.error) return ''; // Handle specific error codes if ('code' in state.error) { switch (state.error.code) { case WalletErrorCode.USER_REJECTED: return 'Connection was cancelled by user'; case WalletErrorCode.UNAUTHORIZED: return 'Wallet access was denied'; case WalletErrorCode.UNSUPPORTED_METHOD: return 'Wallet does not support this operation'; case WalletErrorCode.DISCONNECTED: return 'Wallet is disconnected'; case WalletErrorCode.CHAIN_DISCONNECTED: return 'Please check your network connection'; default: return state.error.message || 'An unknown error occurred'; } } return state.error.message || 'An error occurred'; }, /** * Get current connection status */ getConnectionStatus: (state) => { if (state.isConnecting) return 'connecting'; if (state.isReconnecting) return 'reconnecting'; if (state.isDisconnecting) return 'disconnecting'; if (state.isConnected) return 'connected'; if (state.error) return 'error'; return 'disconnected'; } }; /** * React Context */ const IncentivWalletContext = createContext(undefined); /** * IncentivWalletProvider - Professional React Context Provider * * Implements 3-Tier Reconnection Strategy and provides wagmi-style API * for seamless wallet connection management across the entire app. */ const IncentivWalletProvider = ({ children, config = {}, enableLogging = false }) => { const [state, dispatch] = useReducer(walletReducer, initialWalletState); const [balance, setBalance] = React.useState(undefined); const [isBalanceLoading, setIsBalanceLoading] = React.useState(false); // Service instances const walletService = useRef(null); const initPromise = useRef(null); const isInitialized = useRef(false); const balanceInterval = useRef(null); // Configuration const { contracts = [], autoConnect = true, reconnectOnMount = true, pollingInterval = 30000 // 30 seconds } = config; /** * Initialize wallet service */ const initializeService = useCallback(async () => { if (initPromise.current) { return initPromise.current; } initPromise.current = (async () => { try { if (enableLogging) { } // Migrate storage if needed PersistenceService.migrateStorageIfNeeded(); // Create service instance walletService.current = new IncentivWalletService(); await walletService.current.initialize(); // Register contracts if provided if (contracts.length > 0) { walletService.current.registerContracts(contracts); } // Set up event listeners setupEventListeners(); isInitialized.current = true; if (enableLogging) { } } catch (error) { console.error('❌ Failed to initialize wallet service:', error); dispatch(walletActions.setError(error)); } })(); return initPromise.current; }, [contracts, enableLogging]); /** * Set up event listeners for wallet events */ const setupEventListeners = useCallback(() => { if (!walletService.current) return; // Listen for connection events walletService.current.on('connect', (data) => { dispatch(walletActions.setConnected(data.address)); refreshBalance(); }); // Listen for disconnection events walletService.current.on('disconnect', () => { dispatch(walletActions.setDisconnected()); setBalance(undefined); stopBalancePolling(); }); // Listen for transaction events walletService.current.on('transaction', (data) => { // Refresh balance after transaction setTimeout(refreshBalance, 2000); }); }, [enableLogging]); /** * 3-TIER RECONNECTION STRATEGY */ const attemptReconnection = useCallback(async () => { if (!walletService.current) { await initializeService(); } const service = walletService.current; dispatch(walletActions.setReconnecting()); try { const { address, wasConnected, hasEverConnected } = PersistenceService.loadWalletData(); // Only attempt reconnection if conditions are met if (!address || !wasConnected || !hasEverConnected) { if (enableLogging) { } dispatch(walletActions.setDisconnected()); return; } if (enableLogging) { } // TIER 1: Silent Connection Check const silentAddress = await service.checkSilentConnection(); if (silentAddress && silentAddress.toLowerCase() === address.toLowerCase()) { dispatch(walletActions.setConnected(silentAddress)); if (enableLogging) { } return; } // TIER 2: Session Restore const sessionRestored = await service.tryRestoreSession(address); if (sessionRestored) { dispatch(walletActions.setConnected(address)); if (enableLogging) { } return; } // TIER 3: Check if we should attempt full connect if (PersistenceService.shouldAutoReconnect()) { try { const reconnectedAddress = await service.connect(); if (reconnectedAddress.toLowerCase() === address.toLowerCase()) { dispatch(walletActions.setConnected(reconnectedAddress)); if (enableLogging) { } return; } else { console.warn('⚠️ Reconnected address does not match stored address'); PersistenceService.clearWalletData(); } } catch (connectError) { if (enableLogging) { } } } // All tiers failed dispatch(walletActions.setDisconnected()); if (enableLogging) { } } catch (error) { console.error('❌ Reconnection failed:', error); dispatch(walletActions.setError(error)); } }, [initializeService, enableLogging]); /** * Manual connect function */ const connect = useCallback(async () => { if (!walletService.current) { await initializeService(); } dispatch(walletActions.setConnecting()); try { const address = await walletService.current.connect(); dispatch(walletActions.setConnected(address)); if (enableLogging) { } } catch (error) { console.error('❌ Manual connection failed:', error); dispatch(walletActions.setError(error)); } }, [initializeService, enableLogging]); /** * Disconnect function */ const disconnect = useCallback(() => { var _a; dispatch(walletActions.setDisconnecting()); try { (_a = walletService.current) === null || _a === void 0 ? void 0 : _a.disconnect(); dispatch(walletActions.setDisconnected()); if (enableLogging) { } } catch (error) { console.error('❌ Disconnection failed:', error); dispatch(walletActions.setError(error)); } }, [enableLogging]); /** * Reconnect function */ const reconnect = useCallback(async () => { await attemptReconnection(); }, [attemptReconnection]); /** * Register contracts */ const registerContracts = useCallback((contractConfigs) => { if (!walletService.current) { console.warn('⚠️ Wallet service not initialized, cannot register contracts'); return; } try { walletService.current.registerContracts(contractConfigs); if (enableLogging) { } } catch (error) { console.error('❌ Contract registration failed:', error); dispatch(walletActions.setError(error)); } }, [enableLogging]); /** * Get contract instance */ const getContract = useCallback((name) => { if (!walletService.current) { console.warn('⚠️ Wallet service not initialized'); return null; } try { return walletService.current.getContract(name); } catch (error) { console.error(`❌ Failed to get contract '${name}':`, error); return null; } }, []); /** * Refresh balance */ const refreshBalance = useCallback(async () => { if (!state.isConnected || !state.address || !walletService.current) { return; } setIsBalanceLoading(true); try { const newBalance = await walletService.current.getBalance(state.address); setBalance(newBalance); } catch (error) { console.error('❌ Balance refresh failed:', error); } finally { setIsBalanceLoading(false); } }, [state.isConnected, state.address]); /** * Start balance polling */ const startBalancePolling = useCallback(() => { if (balanceInterval.current) return; balanceInterval.current = setInterval(() => { refreshBalance(); }, pollingInterval); }, [refreshBalance, pollingInterval]); /** * Stop balance polling */ const stopBalancePolling = useCallback(() => { if (balanceInterval.current) { clearInterval(balanceInterval.current); balanceInterval.current = null; } }, []); /** * Initialize on mount */ useEffect(() => { const init = async () => { await initializeService(); if (autoConnect && reconnectOnMount) { await attemptReconnection(); } }; init(); // Cleanup on unmount return () => { var _a; stopBalancePolling(); (_a = walletService.current) === null || _a === void 0 ? void 0 : _a.destroy(); }; }, [initializeService, attemptReconnection, autoConnect, reconnectOnMount, stopBalancePolling]); /** * Handle balance polling based on connection status */ useEffect(() => { if (state.isConnected && state.address) { refreshBalance(); startBalancePolling(); } else { setBalance(undefined); stopBalancePolling(); } }, [state.isConnected, state.address, refreshBalance, startBalancePolling, stopBalancePolling]); /** * Context value */ const contextValue = { // State ...state, // Derived state isReady: walletSelectors.isReady(state), connector: 'incentiv', // Actions connect, disconnect, reconnect, // Contract management registerContracts, getContract, // Balance refreshBalance, balance, isBalanceLoading }; return (jsx(IncentivWalletContext.Provider, { value: contextValue, children: children })); }; /** * Hook to use the wallet context */ const useIncentivWalletContext = () => { const context = React.useContext(IncentivWalletContext); if (context === undefined) { throw new Error('useIncentivWalletContext must be used within an IncentivWalletProvider'); } return context; }; /** * useIncentivWallet - Main wallet hook (wagmi-inspired) * * Primary hook for accessing wallet connection state and actions. * Provides a clean, type-safe API similar to wagmi's useAccount. */ function useIncentivWallet() { const context = useIncentivWalletContext(); return { // Connection state address: context.address, isConnected: context.isConnected, isConnecting: context.isConnecting, isReconnecting: context.isReconnecting, isDisconnecting: context.isDisconnecting, error: context.error, // Balance state balance: context.balance, isBalanceLoading: context.isBalanceLoading, refreshBalance: context.refreshBalance, // Derived state isReady: context.isReady, connector: context.connector, // Actions connect: context.connect, disconnect: context.disconnect, reconnect: context.reconnect }; } /** * useContract - Contract interaction hook * * Provides access to registered smart contracts with full typing support. * Returns the contract instance with a