// 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();
});