Rebuild wallet connect on Reown AppKit (universal, all wallets, working icons)
Replaces the low-level ethereum-provider + deprecated modal (broken icons, jank) with Reown AppKit loaded from the CDN — the standard connector every wallet supports, with QR + mobile deep-links and a polished picker. Keeps the exact SIWE sign-in and contract buy/activate logic, driving AppKit's EIP-1193 provider. CSP widened for the AppKit SDK/RPC + a worker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+80
-146
@@ -1,177 +1,114 @@
|
||||
// Wallet plumbing: EIP-1193 connect, chain add/switch, SIWE sign-in, and raw
|
||||
// calldata builders for the contract's tx functions (no library needed).
|
||||
// Wallet plumbing via Reown AppKit — the universal connector every wallet is
|
||||
// built for (all wallets, QR + mobile deep-links, working icons). AppKit is
|
||||
// lazy-loaded from the CDN on first use; once connected we drive the raw
|
||||
// EIP-1193 provider for chain switch, SIWE sign-in, and contract transactions.
|
||||
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');
|
||||
const APPKIT_URL = 'https://cdn.jsdelivr.net/npm/@reown/appkit-cdn@1.8.23/dist/appkit.js';
|
||||
|
||||
// ── 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 = '<h3 style="margin:0 0 12px">Choose your wallet</h3>';
|
||||
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 ? '<img src="' + d.info.icon + '" alt="" width="22" height="22" style="border-radius:5px">' : '') + '<span>' + ((d.info && d.info.name) || 'Wallet') + '</span>';
|
||||
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 },
|
||||
let modal = null, akPromise = null, provider = null;
|
||||
|
||||
async function initAppKit(c) {
|
||||
if (modal) return modal;
|
||||
if (akPromise) return akPromise;
|
||||
akPromise = (async () => {
|
||||
const mod = await import(APPKIT_URL);
|
||||
const { createAppKit, WagmiAdapter, networks } = mod;
|
||||
const netMap = { 80002: networks.polygonAmoy, 137: networks.polygon };
|
||||
const net = netMap[Number(c.chainId)] || networks.polygonAmoy;
|
||||
const projectId = String(c.walletConnectProjectId || '').trim();
|
||||
const wagmiAdapter = new WagmiAdapter({ networks: [net], projectId });
|
||||
modal = createAppKit({
|
||||
adapters: [wagmiAdapter], networks: [net], projectId, defaultNetwork: net,
|
||||
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' } }
|
||||
url: location.origin, icons: [location.origin + '/logo-icon.png'] },
|
||||
features: { analytics: false, email: false, socials: [] }
|
||||
});
|
||||
return modal;
|
||||
})();
|
||||
return akPromise;
|
||||
}
|
||||
|
||||
function currentAddress() { try { return (modal && modal.getAddress && modal.getAddress()) || null; } catch (e) { return null; } }
|
||||
|
||||
function waitForConnection(timeoutMs) {
|
||||
if (currentAddress()) return Promise.resolve(currentAddress());
|
||||
return new Promise((resolve, reject) => {
|
||||
let done = false, unsub = null;
|
||||
const finish = (addr, err) => { if (done) return; done = true; try { unsub && unsub(); } catch (e) {} err ? reject(err) : resolve(addr); };
|
||||
try { unsub = modal.subscribeAccount(acc => { if (acc && acc.isConnected && acc.address) finish(acc.address); }); } catch (e) {}
|
||||
const t0 = Date.now();
|
||||
(function poll() {
|
||||
if (done) return;
|
||||
const a = currentAddress();
|
||||
if (a) return finish(a);
|
||||
if (Date.now() - t0 > (timeoutMs || 180000)) return finish(null, new Error('Wallet connection timed out. Tap Connect and try again.'));
|
||||
setTimeout(poll, 400);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveProvider() {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
try { const p = modal.getWalletProvider ? await Promise.resolve(modal.getWalletProvider()) : null; if (p && p.request) return p; } catch (e) {}
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
}
|
||||
// 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;
|
||||
throw new Error('Could not reach your wallet. Try connecting again.');
|
||||
}
|
||||
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.');
|
||||
|
||||
function eth() { if (!provider) throw new Error('Connect your wallet first.'); return provider; }
|
||||
|
||||
async function connect() {
|
||||
const c = await IAP.getConfig();
|
||||
await initAppKit(c);
|
||||
let addr = currentAddress();
|
||||
if (!addr) { try { await modal.open(); } catch (e) {} addr = await waitForConnection(180000); }
|
||||
provider = await resolveProvider();
|
||||
await ensureChain(c).catch(() => {}); // AppKit already connects on the right network; switch is best-effort
|
||||
return addr;
|
||||
}
|
||||
|
||||
async function ensureChain(c) {
|
||||
const want = '0x' + Number(c.chainId).toString(16);
|
||||
const cur = await eth().request({ method: 'eth_chainId' });
|
||||
let cur; try { cur = await eth().request({ method: 'eth_chainId' }); } catch (e) { return; }
|
||||
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 (e && e.code !== 4902) return; // don't hard-fail; AppKit/wallet usually handles the network
|
||||
const addParams = { chainId: want, chainName: c.chainName, nativeCurrency: { name: 'POL', symbol: 'POL', decimals: 18 }, rpcUrls: [c.rpc] };
|
||||
if (c.explorer && /^https?:\/\//i.test(c.explorer)) addParams.blockExplorerUrls = [c.explorer];
|
||||
await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] });
|
||||
try { await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] }); } catch (e2) {}
|
||||
}
|
||||
}
|
||||
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.
|
||||
// hex-encode the message (Trust and others require hex for personal_sign)
|
||||
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}
|
||||
return r;
|
||||
}
|
||||
|
||||
async function sendTx(data, valueWei) {
|
||||
const c = await IAP.getConfig();
|
||||
await ensureProvider();
|
||||
const [addr] = await eth().request({ method: 'eth_requestAccounts' });
|
||||
await ensureChain(c);
|
||||
const addr = await connect();
|
||||
const tx = { from: addr, to: c.contract, data };
|
||||
if (valueWei) tx.value = '0x' + BigInt(valueWei).toString(16);
|
||||
// Let the wallet set its own gas/fees. Setting them here makes MetaMask flag
|
||||
// a "site-suggested fee" alert the user can't easily clear; MetaMask's own
|
||||
// estimate already meets Amoy's minimum priority fee.
|
||||
// let the wallet set gas/fees (its estimate meets the chain's minimums)
|
||||
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 };
|
||||
@@ -179,10 +116,9 @@ window.IAPWallet = (function () {
|
||||
}
|
||||
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 value = BigInt(costWei) + BigInt(costWei) / 50n; // 2% oracle-drift pad; contract refunds excess
|
||||
const data = SEL_BUY + pad(productId) + pad(sponsorId || 0);
|
||||
const hash = await sendTx(data, value);
|
||||
return { hash, receipt: await waitTx(hash) };
|
||||
@@ -192,14 +128,12 @@ window.IAPWallet = (function () {
|
||||
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) {}
|
||||
try { if (modal && modal.disconnect) await modal.disconnect(); } catch (e) {}
|
||||
provider = null;
|
||||
try { Object.keys(localStorage).forEach(k => { if (/wc@2|walletconnect|w3m|wcm|reown|wagmi|appkit/i.test(k)) localStorage.removeItem(k); }); } catch (e) {}
|
||||
}
|
||||
|
||||
return { connect, signIn, buy, activate, waitTx, disconnect };
|
||||
})();
|
||||
|
||||
+1
-1
@@ -439,7 +439,7 @@
|
||||
</section>
|
||||
|
||||
<script src="/assets/common.js?v=20260908c"></script>
|
||||
<script src="/assets/wallet.js?v=20260908j"></script>
|
||||
<script src="/assets/wallet.js?v=20260908k"></script>
|
||||
<script src="/assets/home.js?v=20260906m"></script>
|
||||
<script src="/assets/chat.js?v=20260906m"></script>
|
||||
</body>
|
||||
|
||||
+1
-1
@@ -696,7 +696,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/common.js?v=20260908c"></script>
|
||||
<script src="/assets/wallet.js?v=20260908j"></script>
|
||||
<script src="/assets/wallet.js?v=20260908k"></script>
|
||||
<script src="/assets/my.js?v=20260908k"></script>
|
||||
<script src="/assets/chat.js?v=20260907l"></script>
|
||||
</body>
|
||||
|
||||
@@ -162,7 +162,7 @@ const MIME = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': '
|
||||
'.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.webp': 'image/webp',
|
||||
'.ico': 'image/x-icon', '.json': 'application/json', '.mp4': 'video/mp4', '.woff2': 'font/woff2',
|
||||
'.gif': 'image/gif', '.webm': 'video/webm', '.txt': 'text/plain; charset=utf-8', '.xml': 'application/xml; charset=utf-8' };
|
||||
const CSP = "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; media-src 'self' https: blob:; connect-src 'self' https://*.walletconnect.com wss://*.walletconnect.com https://*.walletconnect.org wss://*.walletconnect.org https://*.web3modal.org https://*.reown.com wss://*.reown.com; font-src 'self' data: https://fonts.gstatic.com; form-action 'self'; frame-src https: http:";
|
||||
const CSP = "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; media-src 'self' https: blob:; connect-src 'self' https://*.walletconnect.com wss://*.walletconnect.com https://*.walletconnect.org wss://*.walletconnect.org https://*.reown.com wss://*.reown.com https://*.reown.org wss://*.reown.org https://*.web3modal.org https://*.drpc.org https://*.publicnode.com; font-src 'self' data: https://fonts.gstatic.com; form-action 'self'; frame-src https: http:";
|
||||
function baseHeaders(extra) {
|
||||
return Object.assign({ 'Content-Security-Policy': CSP, 'X-Content-Type-Options': 'nosniff',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin' }, extra || {});
|
||||
|
||||
Reference in New Issue
Block a user