// Wallet plumbing: EIP-1193 connect, chain add/switch, SIWE sign-in, and raw // calldata builders for the contract's tx functions (no library needed). window.IAPWallet = (function () { const SEL_BUY = '0xfd095e97'; // buy(uint32,uint32) const SEL_ACTIVATE = '0x1a93ec95'; // activate(uint32) const pad = v => BigInt(v).toString(16).padStart(64, '0'); // ── multi-wallet: EIP-6963 discovery + a picker; falls back to window.ethereum. // Supports Trust, MetaMask, SafePal, Phantom, OKX, TokenPocket and any 6963 wallet. const discovered = []; try { window.addEventListener('eip6963:announceProvider', e => { const d = e.detail; if (d && d.provider && !discovered.some(x => x.info && d.info && x.info.uuid === d.info.uuid)) discovered.push(d); }); window.dispatchEvent(new Event('eip6963:requestProvider')); } catch (e) {} let CHOSEN = null; function providerList() { const seen = new Set(), out = []; for (const d of discovered) { const k = (d.info && (d.info.rdns || d.info.uuid)) || Math.random(); if (!seen.has(k)) { seen.add(k); out.push(d); } } const add = (p, name) => { if (p && !out.some(x => x.provider === p)) out.push({ info: { name }, provider: p }); }; if (window.ethereum) { if (Array.isArray(window.ethereum.providers)) window.ethereum.providers.forEach(p => add(p, p.isMetaMask ? 'MetaMask' : p.isTrust || p.isTrustWallet ? 'Trust Wallet' : 'Injected wallet')); else add(window.ethereum, window.ethereum.isMetaMask ? 'MetaMask' : window.ethereum.isTrust || window.ethereum.isTrustWallet ? 'Trust Wallet' : 'Injected wallet'); } try { add(window.phantom && window.phantom.ethereum, 'Phantom'); } catch (e) {} add(window.okxwallet, 'OKX Wallet'); try { add(window.tokenpocket && window.tokenpocket.ethereum, 'TokenPocket'); } catch (e) {} add(window.safepal, 'SafePal'); return out; } function pickModal(list) { return new Promise(resolve => { const back = document.createElement('div'); back.style.cssText = 'position:fixed;inset:0;z-index:200;display:grid;place-items:center;background:rgba(2,10,8,.72)'; const card = document.createElement('div'); card.style.cssText = 'background:var(--panel-solid,#0b1512);border:1px solid var(--line-strong,#2c5044);border-radius:16px;padding:20px;max-width:360px;width:92%'; card.innerHTML = '

Choose your wallet

'; const close = v => { if (back.parentNode) document.body.removeChild(back); resolve(v); }; list.forEach(d => { const b = document.createElement('button'); b.className = 'btn sec'; b.type = 'button'; b.style.cssText = 'display:flex;align-items:center;gap:10px;width:100%;justify-content:flex-start;margin:6px 0'; b.innerHTML = (d.info && d.info.icon ? '' : '') + '' + ((d.info && d.info.name) || 'Wallet') + ''; b.addEventListener('click', () => close(d)); card.appendChild(b); }); const cancel = document.createElement('button'); cancel.className = 'btn sec small'; cancel.type = 'button'; cancel.textContent = 'Cancel'; cancel.style.marginTop = '10px'; cancel.addEventListener('click', () => close(null)); card.appendChild(cancel); back.appendChild(card); document.body.appendChild(back); }); } // ── WalletConnect: works in any normal mobile browser (no in-app dApp browser // needed) and on desktop via QR. Lazy-loaded UMD; the returned object is a // plain EIP-1193 provider, so everything downstream is unchanged. let wcSdkPromise = null, wcProvider = null; function loadWcSdk() { const ready = () => (window['@walletconnect/ethereum-provider'] || {}).EthereumProvider; if (ready()) return Promise.resolve(ready()); if (wcSdkPromise) return wcSdkPromise; wcSdkPromise = new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://cdn.jsdelivr.net/npm/@walletconnect/ethereum-provider@2.21.1/dist/index.umd.js'; s.async = true; s.onload = () => { const EP = ready(); EP ? resolve(EP) : reject(new Error('WalletConnect failed to load.')); }; s.onerror = () => reject(new Error('Could not load WalletConnect. Check your connection and try again.')); document.head.appendChild(s); }); return wcSdkPromise; } async function makeWalletConnect(c) { const projectId = String(c.walletConnectProjectId || '').trim(); if (!projectId) throw new Error('WalletConnect is not configured yet.'); const EP = await loadWcSdk(); const chainId = Number(c.chainId); if (!wcProvider) { // optionalChains (not required): testnet wallets like Trust hang or reject // a session that REQUIRES a chain they don't natively list (Amoy 80002). // Optional lets the session establish; we switch/add the chain after. wcProvider = await EP.init({ projectId, optionalChains: [chainId], showQrModal: true, rpcMap: { [chainId]: c.rpc }, metadata: { name: c.siteName || 'InstantAdPay', description: c.tagline || 'Advertise and earn, paid on-chain.', url: location.origin, icons: [location.origin + '/logo-icon.png'], // ask the wallet to bounce back to the dashboard after each approval redirect: { native: '', universal: location.origin + '/my' } } }); } // WalletConnect remembers the last wallet and auto-deep-links to it, which // traps users on a wallet that won't finish (e.g. Trust on a testnet). Clear // that choice so the picker ("All Wallets") shows every time. try { localStorage.removeItem('WALLETCONNECT_DEEPLINK_CHOICE'); } catch (e) {} if (!wcProvider.session) await wcProvider.connect(); // QR on desktop, opens the wallet app on mobile return wcProvider; } async function ensureProvider() { if (CHOSEN && CHOSEN.provider) return CHOSEN.provider; const list = providerList(); let cfg = null; try { cfg = await IAP.getConfig(); } catch (e) {} // WalletConnect only works on chains external wallets recognize (Polygon // mainnet 137 / Amoy testnet 80002). On the private rehearsal chain (31337) // wallets reject the session ("user rejected"), so we keep to the injected // path there and light WC up automatically once we're on a public chain. const WC_CHAINS = [137, 80002]; const wcOn = !!(cfg && String(cfg.walletConnectProjectId || '').trim() && WC_CHAINS.includes(Number(cfg.chainId))); if (wcOn) list.push({ info: { name: 'WalletConnect — scan a QR or open your wallet app', __wc: true } }); if (!list.length) throw new Error('No wallet found. Open this page in your wallet app (Trust, MetaMask, SafePal, Phantom, OKX, TokenPocket) or install a wallet extension.'); const pick = list.length === 1 ? list[0] : await pickModal(list); if (!pick) throw new Error('No wallet selected.'); CHOSEN = (pick.info && pick.info.__wc) ? { info: pick.info, provider: await makeWalletConnect(cfg) } : pick; return CHOSEN.provider; } function eth() { if (CHOSEN && CHOSEN.provider) return CHOSEN.provider; if (window.ethereum) return window.ethereum; throw new Error('No wallet found. Open this page in your wallet app or install a wallet extension.'); } async function ensureChain(c) { const want = '0x' + Number(c.chainId).toString(16); const cur = await eth().request({ method: 'eth_chainId' }); if (cur === want) return; try { await eth().request({ method: 'wallet_switchEthereumChain', params: [{ chainId: want }] }); } catch (e) { if (e.code !== 4902) throw e; const addParams = { chainId: want, chainName: c.chainName, nativeCurrency: { name: 'POL', symbol: 'POL', decimals: 18 }, rpcUrls: [c.rpc] }; // only include a block explorer when it's a real URL — an empty string here // makes wallets (Trust Wallet especially) reject with "invalid method parameters" if (c.explorer && /^https?:\/\//i.test(c.explorer)) addParams.blockExplorerUrls = [c.explorer]; await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] }); } } async function connect() { const c = await IAP.getConfig(); await ensureProvider(); const [addr] = await eth().request({ method: 'eth_requestAccounts' }); await ensureChain(c); return addr; } // SIWE: challenge -> personal_sign -> verify (server sets the session cookie) async function signIn() { const addr = await connect(); const ch = await (await fetch('/api/auth/challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }) })).json(); if (ch.error) throw new Error(ch.error); // hex-encode the message: MetaMask accepts a raw string, but Trust Wallet and // others require hex for personal_sign (else "invalid method parameters"). // The signed bytes are identical, so server-side recovery is unchanged. const hexMsg = '0x' + Array.from(new TextEncoder().encode(ch.message)).map(b => b.toString(16).padStart(2, '0')).join(''); const sig = await eth().request({ method: 'personal_sign', params: [hexMsg, addr] }); const r = await (await fetch('/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr, signature: sig }) })).json(); if (r.error) throw new Error(r.error); return r; // {address, memberId, sponsorId} } async function sendTx(data, valueWei) { const c = await IAP.getConfig(); await ensureProvider(); const [addr] = await eth().request({ method: 'eth_requestAccounts' }); await ensureChain(c); const tx = { from: addr, to: c.contract, data }; if (valueWei) tx.value = '0x' + BigInt(valueWei).toString(16); // Polygon (mainnet + Amoy) enforce a ~25 gwei minimum priority fee; wallets // routinely lowball it and the RPC rejects "gas tip below minimum". Set // explicit EIP-1559 fees above the floor, plus a safe gas limit so the wallet // doesn't mis-estimate. Unused gas is never charged. tx.maxPriorityFeePerGas = '0x' + (30n * 10n ** 9n).toString(16); // 30 gwei (> Polygon's ~25 gwei floor) tx.maxFeePerGas = '0x' + (50n * 10n ** 9n).toString(16); // 50 gwei ceiling (Amoy base fee is ~0; avoids MetaMask "fee too high" alert) tx.gas = '0x' + (600000n).toString(16); // 600k limit return eth().request({ method: 'eth_sendTransaction', params: [tx] }); } async function waitTx(hash) { // poll our own server (CSP-friendly); it relays the receipt from the RPC for (let i = 0; i < 60; i++) { const r = await (await fetch('/api/tx/' + hash)).json(); if (r.found) return { status: r.status, blockNumber: r.blockNumber }; await new Promise(res => setTimeout(res, 2500)); } throw new Error('Timed out waiting for the transaction. Check the explorer.'); } // buy: quote is read live server-side; pad 2% for oracle drift, contract // refunds every wei of excess in the same transaction. async function buy(productId, sponsorId, costWei) { const value = BigInt(costWei) + BigInt(costWei) / 50n; const data = SEL_BUY + pad(productId) + pad(sponsorId || 0); const hash = await sendTx(data, value); return { hash, receipt: await waitTx(hash) }; } async function activate(sponsorId) { const data = SEL_ACTIVATE + pad(sponsorId || 0); const hash = await sendTx(data, null); return { hash, receipt: await waitTx(hash) }; } // Drop the WalletConnect session + any stuck pending request, and forget the // chosen provider so the next connect re-offers the wallet picker. async function disconnect() { try { if (wcProvider && wcProvider.disconnect) await wcProvider.disconnect(); } catch (e) {} wcProvider = null; CHOSEN = null; wcSdkPromise = null; // WalletConnect caches the last-used wallet + session in localStorage; clear // it all so the next connect re-opens the picker instead of auto-reconnecting. try { Object.keys(localStorage).forEach(k => { if (/wc@2|walletconnect|w3m|wcm|reown|ethereum_provider/i.test(k)) localStorage.removeItem(k); }); } catch (e) {} } return { connect, signIn, buy, activate, waitTx, disconnect }; })();