// LockItIn Wallet Connection // Requires: ethers.js, config.php, abi.php // Global state let provider, signer, contract, usdcContract, userAddress; let readOnlyProvider, readOnlyContract, readOnlyUsdc; let protocolFees = { resolutionFee: CONFIG.RESOLUTION_FEE, daoFeeBps: CONFIG.DAO_FEE_BPS, txBuilderFeeBps: CONFIG.TX_BUILDER_FEE_BPS, creationFee: 0 }; // Connection state tracking let isConnecting = false; const walletMenu = document.getElementById('wallet-menu'); // ───────────────────────────────────────────────────────────────────────────── // INJECTED WALLET PROVIDER SELECTION (multi-wallet friendly) // ───────────────────────────────────────────────────────────────────────────── const WALLET_PROVIDER_PREF_KEY = 'lockitin_wallet_provider_pref_v1'; let activeInjectedProvider = null; let activeInjectedProviderLabel = null; const eip6963Providers = []; // { info, provider } // Collect EIP-6963 providers (when supported by wallet extensions) try { window.addEventListener('eip6963:announceProvider', (event) => { const detail = event?.detail; if (!detail?.provider) return; // Deduplicate by reference if (eip6963Providers.some(p => p.provider === detail.provider)) return; eip6963Providers.push(detail); }); // Trigger discovery (wallets respond asynchronously) window.dispatchEvent(new Event('eip6963:requestProvider')); } catch (e) { // Ignore - not supported } function getInjectedProviderLabel(provider, info = null) { if (info?.name) return String(info.name); if (provider?.isMetaMask) return 'MetaMask'; if (provider?.isBraveWallet) return 'Brave Wallet'; if (provider?.isCoinbaseWallet) return 'Coinbase Wallet'; return 'Injected Wallet'; } function getInjectedProviderId(provider, info = null) { const rdns = info?.rdns ? String(info.rdns) : ''; if (rdns) return rdns; if (provider?.isMetaMask) return 'metamask'; if (provider?.isBraveWallet) return 'brave'; if (provider?.isCoinbaseWallet) return 'coinbase'; return 'injected'; } function listInjectedProviders() { const out = []; // Prefer EIP-6963 list (more reliable when multiple wallets are installed) for (const { info, provider } of eip6963Providers) { out.push({ provider, id: getInjectedProviderId(provider, info), label: getInjectedProviderLabel(provider, info), source: 'eip6963', }); } // Fallback: window.ethereum.providers (MetaMask multi-provider pattern) const eth = window.ethereum; const candidates = Array.isArray(eth?.providers) ? eth.providers : (eth ? [eth] : []); for (const p of candidates) { if (!p) continue; if (out.some(x => x.provider === p)) continue; out.push({ provider: p, id: getInjectedProviderId(p, null), label: getInjectedProviderLabel(p, null), source: 'window.ethereum', }); } // De-dupe by id+label (keep first occurrence) const seen = new Set(); return out.filter((p) => { const key = `${p.id}::${p.label}`; if (seen.has(key)) return false; seen.add(key); return true; }); } function pickDefaultProvider(providers) { if (!providers || providers.length === 0) return null; const byId = (id) => providers.find(p => String(p.id).toLowerCase().includes(id)); return byId('metamask') || byId('coinbase') || byId('brave') || providers[0]; } function getEthereumProvider() { return activeInjectedProvider || window.ethereum || null; } window.getEthereumProvider = getEthereumProvider; function updateWalletProviderMenuLabel() { const btn = document.getElementById('wallet-provider-btn'); if (!btn) return; btn.textContent = activeInjectedProviderLabel ? `Wallet Provider: ${activeInjectedProviderLabel}` : 'Wallet Provider'; } async function selectWalletProvider({ forcePrompt = false, allowPrompt = true } = {}) { const providers = listInjectedProviders(); if (!providers.length) { if (typeof uiAlert === 'function') { await uiAlert("No injected wallet provider found. Please install a wallet extension (MetaMask, Coinbase Wallet, Brave Wallet) and refresh."); } return null; } // If only one provider exists, select it silently. if (providers.length === 1) { activeInjectedProvider = providers[0].provider; activeInjectedProviderLabel = providers[0].label; try { localStorage.setItem(WALLET_PROVIDER_PREF_KEY, providers[0].id); } catch (e) {} updateWalletProviderMenuLabel(); return activeInjectedProvider; } // Try previously saved preference first unless forcing a prompt. let preferredId = null; try { preferredId = localStorage.getItem(WALLET_PROVIDER_PREF_KEY); } catch (e) {} if (!forcePrompt && preferredId) { const match = providers.find(p => p.id === preferredId); if (match) { activeInjectedProvider = match.provider; activeInjectedProviderLabel = match.label; updateWalletProviderMenuLabel(); return activeInjectedProvider; } } // Default recommendation const primary = pickDefaultProvider(providers); const secondary = providers.find(p => p.provider !== primary.provider) || providers[0]; // If we can present a 2-choice prompt, do so. if (allowPrompt && providers.length === 2 && typeof showModal === 'function') { const usePrimary = await showModal({ title: 'Choose Wallet Provider', message: `Multiple wallet providers are installed.

Choose which wallet LockItIn should use for this session.

Tip: If transactions fail, disable extra wallet extensions or set your default wallet in Brave settings.`, mode: 'confirm', primaryLabel: primary.label, secondaryLabel: secondary.label, }); const chosen = usePrimary ? primary : secondary; activeInjectedProvider = chosen.provider; activeInjectedProviderLabel = chosen.label; try { localStorage.setItem(WALLET_PROVIDER_PREF_KEY, chosen.id); } catch (e) {} updateWalletProviderMenuLabel(); return activeInjectedProvider; } // 3+ providers: select recommended and show a one-time hint. activeInjectedProvider = primary.provider; activeInjectedProviderLabel = primary.label; try { localStorage.setItem(WALLET_PROVIDER_PREF_KEY, primary.id); } catch (e) {} updateWalletProviderMenuLabel(); if (allowPrompt && typeof uiAlert === 'function') { const labels = providers.map(p => p.label).join(', '); await uiAlert( `Multiple wallet providers detected (${labels}).

LockItIn will use ${primary.label}. To switch, open the wallet menu → “Wallet Provider”.`, 'Wallet Providers' ); } return activeInjectedProvider; } window.chooseWalletProvider = async () => { await selectWalletProvider({ forcePrompt: true }); // If already connected, reconnect using the selected provider. if (isConnected()) { disconnectWallet(); await new Promise(r => setTimeout(r, 150)); await connectWallet(); } }; // RPC cascade configuration const RPC_CASCADE = [ CONFIG.INFURA_RPC_URL, // Primary: Infura 'https://mainnet.base.org', // Fallback 1: Base public 'https://base.llamarpc.com' // Fallback 2: llama ].filter(url => url && url.length > 0); // Track which RPC is currently active (for logging/debugging) let activeRpcIndex = 0; // Read-only provider/contract for viewing when wallet isn't connected // Uses async initialization with actual health checks let readOnlyProviderPromise = null; function getReadOnlyProvider() { // Return cached provider if available if (readOnlyProvider) return readOnlyProvider; // Synchronous fallback: create provider without health check // (async init will replace it if needed) if (!readOnlyProvider) { try { readOnlyProvider = new ethers.JsonRpcProvider(RPC_CASCADE[0]); } catch (e) { console.warn("Failed to init read-only provider sync", e); } } // Kick off async health check in background (non-blocking) if (!readOnlyProviderPromise) { readOnlyProviderPromise = initReadOnlyProviderWithFallback(); } return readOnlyProvider; } // Async provider initialization with health checks and fallback async function initReadOnlyProviderWithFallback() { for (let i = 0; i < RPC_CASCADE.length; i++) { const rpcUrl = RPC_CASCADE[i]; try { const prov = new ethers.JsonRpcProvider(rpcUrl); // Quick health check: get block number (lightweight) await Promise.race([ prov.getBlockNumber(), new Promise((_, reject) => setTimeout(() => reject(new Error('RPC timeout')), 5000)) ]); // Success! Use this provider readOnlyProvider = prov; activeRpcIndex = i; if (i > 0) { console.log(`RPC fallback: using ${rpcUrl} (primary failed)`); } // Rebuild contracts with working provider readOnlyContract = new ethers.Contract(CONFIG.CONTRACT_ADDRESS, LOCKITIN_PROTOCOL_ABI, prov); readOnlyUsdc = new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, prov); return prov; } catch (e) { console.warn(`RPC ${rpcUrl} failed:`, e.message); continue; } } console.error("All RPCs failed!"); return null; } // Get a working provider, with automatic fallback on failure async function getWorkingProvider() { // Wait for async init if in progress if (readOnlyProviderPromise) { await readOnlyProviderPromise; } return readOnlyProvider; } function getReadOnlyContract() { // Always prefer wallet provider when connected - it's more reliable if (contract) return contract; // Fall back to read-only provider for disconnected users if (readOnlyContract) return readOnlyContract; const ro = getReadOnlyProvider(); if (!ro) return null; try { readOnlyContract = new ethers.Contract(CONFIG.CONTRACT_ADDRESS, LOCKITIN_PROTOCOL_ABI, ro); readOnlyUsdc = new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, ro); } catch (e) { console.warn("Failed to init read-only contract", e); } return readOnlyContract; } // Explicit public-RPC reader (never uses the wallet provider) function getPublicReadContract() { if (readOnlyContract) return readOnlyContract; const ro = getReadOnlyProvider(); if (!ro) return null; try { readOnlyContract = new ethers.Contract(CONFIG.CONTRACT_ADDRESS, LOCKITIN_PROTOCOL_ABI, ro); readOnlyUsdc = new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, ro); } catch (e) { console.warn("Failed to init public read-only contract", e); } return readOnlyContract; } // Alias for clarity function getContract() { return getReadOnlyContract(); } // Explicit reader that always uses the public RPC (never the wallet RPC) function getReadContract() { // Prefer the connected wallet RPC (more reliable and avoids shared public RPC rate limits) if (contract) return contract; // Fall back to dedicated public reader for disconnected users const pub = getPublicReadContract(); if (pub) return pub; return null; } // Ensure signer + contracts are initialized; reconnect if needed async function ensureContractsReady() { // Helper to test if contract is actually functional async function testContractWorks() { if (!contract || !signer) return false; try { // Test signer can get address (basic sanity) const addr = await signer.getAddress(); if (!addr) return false; return true; } catch (e) { console.warn("Contract/signer test failed:", e.message); return false; } } // 1. Check if existing setup works if (contract && usdcContract && signer) { const works = await testContractWorks(); if (works) return true; // Contract exists but broken - force rebuild console.warn("Contract exists but not working, forcing rebuild..."); contract = null; usdcContract = null; signer = null; provider = null; } try { // 2. Force fresh connection on mobile const eth = getEthereumProvider(); if (eth) { console.log("Building fresh provider/signer/contract..."); provider = new ethers.BrowserProvider(eth); // Request accounts to ensure we're connected await provider.send("eth_requestAccounts", []); signer = await provider.getSigner(); userAddress = await signer.getAddress(); contract = new ethers.Contract(CONFIG.CONTRACT_ADDRESS, LOCKITIN_PROTOCOL_ABI, signer); usdcContract = new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, signer); // Verify it works const works = await testContractWorks(); if (works) { console.log("Fresh contract verified working"); updateWalletUI(); return true; } console.error("Fresh contract still not working!"); } } catch (e) { console.warn("ensureContractsReady recovery failed", e); } return !!(contract && usdcContract && signer); } // Expose helper window.ensureContractsReady = ensureContractsReady; function updateWalletUI() { const btn = document.getElementById('btn-connect'); if (!btn) return; if (userAddress) { try { const shortAddr = userAddress.slice(0, 6) + '...' + userAddress.slice(-4); btn.innerText = shortAddr; // Also update create button state if on that tab if (window.updateCreateButtonState) window.updateCreateButtonState(); } catch (e) { console.error("UI Update failed", e); } } else { btn.innerText = "CONNECT WALLET"; } } async function connectWallet({ requestPermissions = true } = {}) { // Prevent concurrent connection attempts if (isConnecting) { console.log('[Wallet] Connection already in progress, skipping'); return; } // If already connected, clicking the button allows switching accounts if (isConnected() && requestPermissions) { try { const eth = getEthereumProvider(); if (!eth) { await uiAlert("No wallet provider found. Please refresh and try again."); return; } await eth.request({ method: "wallet_requestPermissions", params: [{ eth_accounts: {} }] }); // The accountsChanged event will trigger the UI update return; } catch (e) { console.log("Wallet switch cancelled", e); return; } } // Pick an injected provider (handles Brave Wallet + MetaMask conflicts) const eth = await selectWalletProvider({ allowPrompt: requestPermissions }); if (!eth) { await uiAlert("Please install MetaMask, Coinbase Wallet, or Brave Wallet"); return; } bindWalletEventListeners(eth); isConnecting = true; try { // Always create fresh provider to avoid stale state issues provider = new ethers.BrowserProvider(eth); // Request accounts - this prompts the user to connect let accounts; try { accounts = await provider.send(requestPermissions ? "eth_requestAccounts" : "eth_accounts", []); } catch (reqErr) { // User rejected or wallet issue if (reqErr.code === 4001) { console.log("User rejected connection request"); return; } throw reqErr; } if (!accounts || accounts.length === 0) { console.warn("No accounts returned from wallet"); return; } signer = await provider.getSigner(); userAddress = await signer.getAddress(); console.log("[Wallet] Connected:", userAddress); // Reset new user cache (for education hints) if (window.resetNewUserCache) window.resetNewUserCache(); // Update connect button immediately updateWalletUI(); // Check network and switch to Base if needed let network = await provider.getNetwork(); if (network.chainId !== CONFIG.BASE_CHAIN_ID_DECIMAL) { console.log(`[Wallet] Wrong network (${network.chainId}), switching to Base...`); // Set flag to prevent reload loop from chainChanged event switchingChain = true; let switchSucceeded = false; try { await eth.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: CONFIG.BASE_CHAIN_ID }], }); switchSucceeded = true; } catch (err) { if (err.code === 4902) { // Chain not added, try to add it try { await eth.request({ method: 'wallet_addEthereumChain', params: [{ chainId: CONFIG.BASE_CHAIN_ID, chainName: 'Base', nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, rpcUrls: ['https://mainnet.base.org'], blockExplorerUrls: ['https://basescan.org'] }], }); switchSucceeded = true; } catch (addErr) { switchingChain = false; isConnecting = false; await uiAlert("Please add Base network to your wallet and try again."); return; } } else if (err.code === 4001) { // User rejected the switch switchingChain = false; isConnecting = false; await uiAlert("Please switch to Base network to use LockItIn."); return; } else { switchingChain = false; isConnecting = false; await uiAlert("Please switch to Base network in your wallet."); return; } } if (switchSucceeded) { // CRITICAL: After network switch, re-create provider and signer // Mobile wallets need time to complete the switch console.log('[Wallet] Network switch requested, reinitializing...'); await new Promise(resolve => setTimeout(resolve, 500)); // Fresh provider on new network provider = new ethers.BrowserProvider(eth); signer = await provider.getSigner(); userAddress = await signer.getAddress(); // Verify we're now on Base network = await provider.getNetwork(); if (network.chainId !== CONFIG.BASE_CHAIN_ID_DECIMAL) { console.error('[Wallet] Still on wrong network:', network.chainId); switchingChain = false; isConnecting = false; await uiAlert("Network switch didn't complete. Please switch to Base manually and refresh the page."); return; } console.log('[Wallet] Successfully on Base network'); } // Clear the flag after successful switch switchingChain = false; } // Initialize contracts with the signer contract = new ethers.Contract(CONFIG.CONTRACT_ADDRESS, LOCKITIN_PROTOCOL_ABI, signer); usdcContract = new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, signer); // Clear read-only contract so we use wallet provider going forward readOnlyContract = null; readOnlyUsdc = null; // Load protocol fees from contract (non-blocking) loadProtocolFees().catch(e => console.warn("Fee load failed:", e)); // Refresh current view after wallet connection // Use the centralized view system if available if (typeof isViewingCommitment === 'function' && isViewingCommitment()) { // User is viewing a specific commitment - reload it (they may have connected to accept) const commitmentId = getCurrentCommitmentId ? getCurrentCommitmentId() : null; if (commitmentId !== null && typeof loadSingleCommitment === 'function') { loadSingleCommitment(commitmentId); } } else if (typeof isOnBrowseTab === 'function' && isOnBrowseTab()) { // On browse list - refresh the list if (typeof loadCommitments === 'function') { loadCommitments(); } } // If on Create tab, no data refresh needed // Ensure UI reflects connected state updateWalletUI(); loadWalletBalances(); if (window.updateCreateButtonState) window.updateCreateButtonState(); if (window.updateMyPositionsButton) window.updateMyPositionsButton(); console.log("[Wallet] Connection complete"); isConnecting = false; } catch (err) { isConnecting = false; console.error("Wallet connection error:", err); // Don't show alert for user rejections if (err.code === 4001 || err.code === 'ACTION_REJECTED') { return; } // Handle specific error types gracefully const msg = err.message || String(err); if (msg.includes('BigNumberish') || msg.includes('INVALID_ARGUMENT')) { // RPC parsing errors - usually recoverable console.warn("RPC parsing issue, may recover on retry"); return; } // Show user-friendly error const friendlyMsg = msg.includes('network') ? "Network error. Please check your connection and try again." : "Connection failed. Please try again."; if (window.uiModal?.alert) { await uiAlert(friendlyMsg); } else { showError('create-error', friendlyMsg); } } } async function loadProtocolFees() { try { const [resFee, daoBps, txBuilderBps, createFee] = await Promise.all([ contract.resolutionFee(), contract.daoFeeBps(), contract.txBuilderFeeBps(), contract.creationFee() ]); protocolFees.resolutionFee = Number(resFee); protocolFees.daoFeeBps = Number(daoBps); protocolFees.txBuilderFeeBps = Number(txBuilderBps); protocolFees.creationFee = Number(createFee); // Update UI for creation fee if non-zero const feeDisplayEl = document.getElementById('fee-display'); const creationFeeAmountEl = document.getElementById('creation-fee-amount'); if (protocolFees.creationFee > 0) { if (feeDisplayEl) feeDisplayEl.style.display = 'block'; if (creationFeeAmountEl) creationFeeAmountEl.innerText = '$' + (protocolFees.creationFee / 1_000_000).toFixed(2); } else { if (feeDisplayEl) feeDisplayEl.style.display = 'none'; } // Update UI for dynamic fees in info-box const daoFeeEl = document.getElementById('display-dao-fee'); if (daoFeeEl) daoFeeEl.innerText = `${(protocolFees.daoFeeBps / 100).toFixed(1)}%`; const txBuilderFeeEl = document.getElementById('display-tx-builder-fee'); if (txBuilderFeeEl) txBuilderFeeEl.innerText = `${(protocolFees.txBuilderFeeBps / 100).toFixed(1)}%`; const resFeeEl = document.getElementById('display-resolution-fee'); if (resFeeEl) resFeeEl.innerText = `${(protocolFees.resolutionFee / 1_000_000).toFixed(2)} USDC`; } catch (e) { console.warn("Could not load fees, using defaults", e); } } function isConnected() { return !!signer; } function handleWalletButtonClick(e) { e?.stopPropagation(); if (!isConnected()) { connectWallet(); return; } toggleWalletMenu(); } function toggleWalletMenu(force) { if (!walletMenu) return; const show = typeof force === 'boolean' ? force : !walletMenu.classList.contains('show'); walletMenu.classList.toggle('show', show); } function hideWalletMenu() { toggleWalletMenu(false); } function disconnectWallet() { // Clear TOS cache for this wallet before clearing address if (window.clearTosCache && userAddress) { window.clearTosCache(userAddress); } // Clear all wallet state userAddress = null; signer = null; provider = null; contract = null; usdcContract = null; updateWalletUI(); hideWalletMenu(); // Hide balances const balancesEl = document.getElementById('wallet-balances'); if (balancesEl) balancesEl.style.display = 'none'; // Hide My Positions button and close view if open if (window.updateMyPositionsButton) window.updateMyPositionsButton(); if (window.closeMyPositions) window.closeMyPositions(); // Leave read-only contract intact for browsing; wallet contract cleared. } async function copyAddress() { if (!userAddress) return; try { await navigator.clipboard.writeText(userAddress); // Brief visual feedback on the button const btn = document.querySelector('#wallet-menu .wallet-menu-item'); if (btn) { const original = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = original; }, 1500); } } catch (e) { console.warn('Clipboard write failed', e); } hideWalletMenu(); } async function loadWalletBalances() { const balancesEl = document.getElementById('wallet-balances'); if (!balancesEl) return; if (!userAddress) { balancesEl.style.display = 'none'; return; } // Show loading state balancesEl.style.display = 'block'; balancesEl.innerHTML = '...'; try { const prov = provider || getReadOnlyProvider(); if (!prov) return; // Fetch ETH and USDC balances in parallel const usdcReader = usdcContract || new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, prov); const [ethBal, usdcBal] = await Promise.all([ prov.getBalance(userAddress), usdcReader.balanceOf(userAddress) ]); // Format: ETH to 4 decimals, USDC to 2 decimals const ethFormatted = parseFloat(ethers.formatEther(ethBal)).toFixed(4); const usdcFormatted = parseFloat(ethers.formatUnits(usdcBal, 6)).toFixed(2); balancesEl.innerHTML = `ETH: ${ethFormatted}USDC: $${usdcFormatted}`; } catch (e) { console.warn('Failed to load balances', e); balancesEl.style.display = 'none'; } } // Expose for external calls window.loadWalletBalances = loadWalletBalances; window.activeRpcIndex = activeRpcIndex; // Expose window.RPC_CASCADE = RPC_CASCADE; // Expose async function requireConnection() { if (!isConnected()) { // Auto-connect instead of just showing an alert const eth = getEthereumProvider(); if (eth) { await connectWallet({ requestPermissions: true }); // Check if connection succeeded if (!isConnected()) { return false; // User rejected or connection failed } } else { await uiAlert("Please install MetaMask or Coinbase Wallet"); return false; } } // Ensure contracts are initialized (handles edge cases where signer exists but contract doesn't) const ready = await ensureContractsReady(); if (!ready) { await uiAlert("Wallet connection issue. Please reconnect."); return false; } return true; } // Track if we're in the middle of a programmatic chain switch let switchingChain = false; let walletListenersProvider = null; function bindWalletEventListeners(eth) { if (!eth || typeof eth.on !== 'function') return; if (walletListenersProvider === eth) return; walletListenersProvider = eth; eth.on('accountsChanged', (accounts) => { if (accounts.length === 0) { // User disconnected via wallet disconnectWallet(); } else { const newAddr = accounts[0].toLowerCase(); const currentAddr = userAddress?.toLowerCase(); // Only act if address actually changed (or we weren't connected) if (!currentAddr || newAddr !== currentAddr) { console.log('[Wallet] Account changed:', accounts[0]); // Only reconnect if not mid-connection and not mid-chain-switch if (!isConnecting && !switchingChain) { connectWallet({ requestPermissions: false }); } } // If same account, no action needed } }); eth.on('chainChanged', (_chainId) => { // If we triggered the switch programmatically, don't reload // The connectWallet function handles reinitialization if (switchingChain) { console.log('[Wallet] Chain changed during programmatic switch, skipping reload'); return; } // User manually changed chain - reload to reinitialize window.location.reload(); }); } // Auto-connect on idle if authorized (avoids blocking first paint) let autoConnectScheduled = false; function scheduleAutoConnect() { if (autoConnectScheduled) return; autoConnectScheduled = true; const run = async () => { const eth = await selectWalletProvider({ allowPrompt: false }); if (!eth) return; bindWalletEventListeners(eth); try { const accounts = await eth.request({ method: 'eth_accounts' }); if (accounts && accounts.length > 0) { console.log("Auto-connecting wallet..."); connectWallet({ requestPermissions: false }); } } catch (e) { console.warn("Auto-connect failed", e); } }; if (typeof requestIdleCallback === 'function') { requestIdleCallback(() => run(), { timeout: 2000 }); } else { setTimeout(run, 500); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', scheduleAutoConnect); } else { scheduleAutoConnect(); } // EIP-2612 Permit helpers for gasless USDC approvals // USDC on Base EIP-712 domain const USDC_PERMIT_DOMAIN = { name: "USD Coin", version: "2", chainId: 8453, verifyingContract: CONFIG.USDC_ADDRESS }; const PERMIT_TYPES = { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" } ] }; async function getPermitNonce(owner) { // USDC uses nonces(address) function const signerOrProvider = signer || provider; const usdcWithNonces = new ethers.Contract( CONFIG.USDC_ADDRESS, [...USDC_ABI, "function nonces(address owner) view returns (uint256)"], signerOrProvider ); const nonce = await usdcWithNonces.nonces(owner); return nonce; } async function signPermit(owner, spender, value, nonce, deadline) { const permitData = { owner: owner, spender: spender, value: value.toString(), nonce: nonce.toString(), deadline: deadline }; console.log("Signing permit...", permitData); // Sign typed data using EIP-712 let signature; try { signature = await signer.signTypedData( USDC_PERMIT_DOMAIN, PERMIT_TYPES, permitData ); } catch (e) { console.error("signTypedData failed:", e); // Handle user rejection specifically if (e.code === 4001 || e.code === 'ACTION_REJECTED') { throw new Error("User rejected signature"); } throw e; } console.log("signTypedData returned:", signature, typeof signature); // Validation for Brave/legacy wallets that might return odd values if (!signature) { throw new Error("Wallet returned empty signature"); } // If signature is an empty array (Brave edge case?), reject it if (Array.isArray(signature) && signature.length === 0) { throw new Error("Wallet returned invalid signature (empty array)"); } // Split signature into v, r, s components let sig; try { sig = ethers.Signature.from(signature); } catch (e) { console.error("ethers.Signature.from failed:", e); throw new Error(`Failed to parse signature: ${e.message}`); } return { v: sig.v, r: sig.r, s: sig.s }; } /** * Handle wallet authorization errors (code 4100) * Attempts to reconnect automatically, returns user-friendly message if it fails * @param {Error} err - The caught error * @returns {Promise<{reconnected: boolean, message: string}>} */ async function handleWalletAuthError(err) { // Check if this is a 4100 authorization error const is4100 = ( err?.code === 4100 || err?.error?.code === 4100 || err?.message?.includes('4100') || err?.message?.includes('not been authorized') ); if (!is4100) { return { reconnected: false, message: null }; } console.log('[Wallet] Detected 4100 auth error, attempting reconnect...'); // Try to reconnect try { const eth = getEthereumProvider(); if (eth) { // Request accounts again to trigger wallet authorization prompt const accounts = await eth.request({ method: 'eth_requestAccounts' }); if (accounts && accounts.length > 0) { // Reinitialize provider and signer provider = new ethers.BrowserProvider(eth); signer = await provider.getSigner(); userAddress = await signer.getAddress(); // Reinitialize contracts with new signer contract = new ethers.Contract(CONFIG.CONTRACT_ADDRESS, LOCKITIN_PROTOCOL_ABI, signer); usdcContract = new ethers.Contract(CONFIG.USDC_ADDRESS, USDC_ABI, signer); console.log('[Wallet] Reconnected successfully:', userAddress); updateWalletUI(); return { reconnected: true, message: null }; } } } catch (reconnectErr) { console.warn('[Wallet] Reconnect attempt failed:', reconnectErr); } // Reconnect failed - return friendly message return { reconnected: false, message: 'Wallet not authorized. Please disconnect and reconnect your wallet, or unlock it and try again.' }; } // Expose helpers window.getReadOnlyContract = getReadOnlyContract; window.getWorkingProvider = getWorkingProvider; window.connectWallet = connectWallet; window.disconnectWallet = disconnectWallet; window.copyAddress = copyAddress; window.hideWalletMenu = hideWalletMenu; window.handleWalletButtonClick = handleWalletButtonClick; window.getPermitNonce = getPermitNonce; window.signPermit = signPermit; window.updateWalletUI = updateWalletUI; window.handleWalletAuthError = handleWalletAuthError; // Close wallet menu on outside click document.addEventListener('click', (e) => { if (!walletMenu) return; const btn = document.getElementById('btn-connect'); if (walletMenu.contains(e.target) || btn?.contains(e.target)) return; hideWalletMenu(); });