From 7b131dad28296c141c5430e5dd89b876ecd54fa3 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 8 Sep 2026 10:49:34 -0500 Subject: [PATCH] Gas floor fix, chain guard, and dashboard footer - wallet.js: fetch correct EIP-1559 fees from the network (Amoy/Polygon Bor enforce a ~25-30 gwei priority floor MetaMask's estimate misses) via new /api/gas; hard chain-guard before signing so a tx never lands on the wrong network; accept the wallet's usual networks in AppKit so its modal stops looping and drive add+switch ourselves. - chain.js: suggestedFees() from eth_maxPriorityFeePerGas + base fee. - Dashboard: move the Site links (Ad packages / Live ledger / The contract) out of the sidebar into a page footer; hide the sidebar scrollbar. Co-Authored-By: Claude Opus 4.8 --- chain.js | 18 +++++++++++++++++- public/assets/site.css | 6 +++++- public/assets/wallet.js | 39 +++++++++++++++++++++++++++++++++++---- public/index.html | 2 +- public/my.html | 15 +++++++-------- server.js | 5 +++++ 6 files changed, 70 insertions(+), 15 deletions(-) diff --git a/chain.js b/chain.js index 39d24e7..a394b01 100644 --- a/chain.js +++ b/chain.js @@ -235,5 +235,21 @@ function init(opts) { setInterval(tail, POLL_MS); } +// Recommended EIP-1559 fees straight from the network. Polygon Amoy's Bor nodes +// enforce a ~25 gwei minimum priority fee, but wallets (notably MetaMask) apply +// a stale low estimate and get the raw tx rejected ("gas tip below minimum"). +// We hand the wallet correct fees so the tx clears the floor: a priority tip at +// or above the network suggestion (floored at 30 gwei for headroom) and a +// maxFee that covers 2x base + tip so MetaMask never flags "max fee too low". +async function suggestedFees() { + let tip = 0n, base = 0n; + try { tip = BigInt(await rpc('eth_maxPriorityFeePerGas', [])); } catch (e) {} + try { const blk = await rpc('eth_getBlockByNumber', ['latest', false]); base = BigInt((blk && blk.baseFeePerGas) || '0x0'); } catch (e) {} + const MIN_TIP = 30000000000n; // 30 gwei — safely over Amoy's ~25 gwei floor + const priority = tip > MIN_TIP ? tip : MIN_TIP; + const maxFee = base * 2n + priority; + return { maxPriorityFeePerGas: '0x' + priority.toString(16), maxFeePerGas: '0x' + maxFee.toString(16) }; +} + module.exports = { init, getConfig, reloadConfig, memberIdByAccount, memberCount, member, - product, productCount, quoteWei, creditBalance, catalog, recentEvents, totals, rpc, decodeLog }; + product, productCount, quoteWei, creditBalance, catalog, recentEvents, totals, rpc, decodeLog, suggestedFees }; diff --git a/public/assets/site.css b/public/assets/site.css index a323681..2c5ebca 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -227,7 +227,7 @@ textarea{resize:vertical;font:inherit} /* ── member back-office shell ─────────────────────────── */ .bo-body{background:var(--ground)} .bo{display:grid;grid-template-columns:236px 1fr;min-height:100vh} -.bo-side{position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column;gap:22px; +.bo-side{position:sticky;top:0;height:100vh;overflow-y:auto;scrollbar-width:none;-ms-overflow-style:none;display:flex;flex-direction:column;gap:22px; padding:22px 16px;background:rgba(10,18,15,.92);border-right:1px solid var(--line)} .bo-side .logo{font-size:19px;padding:0 8px} .bo-menu{display:flex;flex-direction:column;gap:4px} @@ -243,6 +243,10 @@ textarea{resize:vertical;font:inherit} .bo-links a:hover{color:var(--ink);text-decoration:none} .bo-foot{margin-top:auto;padding:14px 12px 0;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:6px;overflow-wrap:anywhere} .bo-main{min-width:0;display:flex;flex-direction:column} +.bo-side::-webkit-scrollbar{width:0;height:0;display:none} +.bo-pagefoot{margin-top:auto;padding:22px 26px;border-top:1px solid var(--line);display:flex;gap:20px;flex-wrap:wrap;justify-content:center;font-size:13px} +.bo-pagefoot a{color:var(--muted)} +.bo-pagefoot a:hover{color:var(--ink);text-decoration:none} .bo-top{display:flex;align-items:center;gap:16px;padding:16px 26px;border-bottom:1px solid var(--line); background:rgba(6,10,8,.75);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);position:sticky;top:0;z-index:5} .bo-top h2{font-size:20px} diff --git a/public/assets/wallet.js b/public/assets/wallet.js index 4b2cafc..9b06470 100644 --- a/public/assets/wallet.js +++ b/public/assets/wallet.js @@ -26,10 +26,18 @@ window.IAPWallet = (function () { // and the failed mobile buy (the chain switch never completed). const rpc = String(c.rpc || '').trim(); const net = rpc ? Object.assign({}, base, { rpcUrls: { default: { http: [rpc] }, public: { http: [rpc] } } }) : base; + // Accept the wallet's usual networks too, so AppKit doesn't trap the user in + // its own "Switch Network" modal — that modal loops on a testnet the wallet + // can't auto-add. We switch to `net` ourselves (wallet_addEthereumChain adds + // + switches in one step) and sendTx hard-guards the chain before signing. + const allNets = [net]; + for (const k of ['polygon', 'mainnet']) { + try { const n = networks[k]; if (n && n.id !== net.id) allNets.push(n); } catch (e) {} + } const projectId = String(c.walletConnectProjectId || '').trim(); - const wagmiAdapter = new WagmiAdapter({ networks: [net], projectId }); + const wagmiAdapter = new WagmiAdapter({ networks: allNets, projectId }); modal = createAppKit({ - adapters: [wagmiAdapter], networks: [net], projectId, defaultNetwork: net, + adapters: [wagmiAdapter], networks: allNets, 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'] }, features: { analytics: false, email: false, socials: [] } @@ -85,7 +93,9 @@ window.IAPWallet = (function () { try { await eth().request({ method: 'wallet_switchEthereumChain', params: [{ chainId: want }] }); } catch (e) { - if (e && e.code !== 4902) return; // don't hard-fail; AppKit/wallet usually handles the network + // any failure (not just 4902): try to add the chain — wallet_addEthereumChain + // adds AND switches in one step, which is what unblocks wallets that can't + // otherwise reach a chain they don't already have (e.g. a testnet). 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]; try { await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] }); } catch (e2) {} @@ -110,9 +120,30 @@ window.IAPWallet = (function () { async function sendTx(data, valueWei) { const c = await IAP.getConfig(); const addr = await connect(); + // Hard chain guard: connect()'s switch is best-effort and some wallets (or + // AppKit's own modal) don't complete it. Never sign on the wrong chain — + // a value tx to a contract that doesn't exist on that chain would look like + // it "succeeded" while doing nothing. + const want = '0x' + Number(c.chainId).toString(16); + let cur; try { cur = await eth().request({ method: 'eth_chainId' }); } catch (e) {} + if (cur && cur.toLowerCase() !== want.toLowerCase()) { + await ensureChain(c); + try { cur = await eth().request({ method: 'eth_chainId' }); } catch (e) {} + if (cur && cur.toLowerCase() !== want.toLowerCase()) + throw new Error('Your wallet is on the wrong network. Switch it to ' + (c.chainName || 'the correct network') + ', then try again.'); + } const tx = { from: addr, to: c.contract, data }; if (valueWei) tx.value = '0x' + BigInt(valueWei).toString(16); - // let the wallet set gas/fees (its estimate meets the chain's minimums) + // Amoy's Bor nodes enforce a ~25 gwei minimum priority fee that MetaMask's + // own estimate misses ("gas tip below minimum"). Pull the network's correct + // fees from the server and set them so the tx clears the floor. + try { + const g = await (await fetch('/api/gas')).json(); + if (g && g.maxPriorityFeePerGas && g.maxFeePerGas) { + tx.maxPriorityFeePerGas = g.maxPriorityFeePerGas; + tx.maxFeePerGas = g.maxFeePerGas; + } + } catch (e) {} return eth().request({ method: 'eth_sendTransaction', params: [tx] }); } diff --git a/public/index.html b/public/index.html index 1368b15..a90e407 100644 --- a/public/index.html +++ b/public/index.html @@ -439,7 +439,7 @@ - + diff --git a/public/my.html b/public/my.html index 2aba520..4b53363 100644 --- a/public/my.html +++ b/public/my.html @@ -5,7 +5,7 @@ Member area | InstantAdPay - + @@ -140,12 +140,6 @@ Team Messages - + @@ -696,7 +695,7 @@ - + diff --git a/server.js b/server.js index cca59a8..3c283e9 100644 --- a/server.js +++ b/server.js @@ -503,6 +503,11 @@ const server = http.createServer(async (req, res) => { await auth.logout(req); return json(res, 200, { ok: true }, { 'Set-Cookie': auth.clearCookie() }); } + if (p === '/api/gas' && req.method === 'GET') { + try { return json(res, 200, await chain.suggestedFees()); } + catch (e) { return json(res, 200, {}); } + } + if (p === '/api/me' && req.method === 'GET') { const s = await auth.fromRequest(req); if (!s) return json(res, 200, { signedIn: false });