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 <noreply@anthropic.com>
This commit is contained in:
@@ -235,5 +235,21 @@ function init(opts) {
|
|||||||
setInterval(tail, POLL_MS);
|
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,
|
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 };
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ textarea{resize:vertical;font:inherit}
|
|||||||
/* ── member back-office shell ─────────────────────────── */
|
/* ── member back-office shell ─────────────────────────── */
|
||||||
.bo-body{background:var(--ground)}
|
.bo-body{background:var(--ground)}
|
||||||
.bo{display:grid;grid-template-columns:236px 1fr;min-height:100vh}
|
.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)}
|
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-side .logo{font-size:19px;padding:0 8px}
|
||||||
.bo-menu{display:flex;flex-direction:column;gap:4px}
|
.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-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-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-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);
|
.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}
|
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}
|
.bo-top h2{font-size:20px}
|
||||||
|
|||||||
+35
-4
@@ -26,10 +26,18 @@ window.IAPWallet = (function () {
|
|||||||
// and the failed mobile buy (the chain switch never completed).
|
// and the failed mobile buy (the chain switch never completed).
|
||||||
const rpc = String(c.rpc || '').trim();
|
const rpc = String(c.rpc || '').trim();
|
||||||
const net = rpc ? Object.assign({}, base, { rpcUrls: { default: { http: [rpc] }, public: { http: [rpc] } } }) : base;
|
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 projectId = String(c.walletConnectProjectId || '').trim();
|
||||||
const wagmiAdapter = new WagmiAdapter({ networks: [net], projectId });
|
const wagmiAdapter = new WagmiAdapter({ networks: allNets, projectId });
|
||||||
modal = createAppKit({
|
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.',
|
metadata: { name: c.siteName || 'InstantAdPay', description: c.tagline || 'Advertise and earn, paid on-chain.',
|
||||||
url: location.origin, icons: [location.origin + '/logo-icon.png'] },
|
url: location.origin, icons: [location.origin + '/logo-icon.png'] },
|
||||||
features: { analytics: false, email: false, socials: [] }
|
features: { analytics: false, email: false, socials: [] }
|
||||||
@@ -85,7 +93,9 @@ window.IAPWallet = (function () {
|
|||||||
try {
|
try {
|
||||||
await eth().request({ method: 'wallet_switchEthereumChain', params: [{ chainId: want }] });
|
await eth().request({ method: 'wallet_switchEthereumChain', params: [{ chainId: want }] });
|
||||||
} catch (e) {
|
} 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] };
|
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];
|
if (c.explorer && /^https?:\/\//i.test(c.explorer)) addParams.blockExplorerUrls = [c.explorer];
|
||||||
try { await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] }); } catch (e2) {}
|
try { await eth().request({ method: 'wallet_addEthereumChain', params: [addParams] }); } catch (e2) {}
|
||||||
@@ -110,9 +120,30 @@ window.IAPWallet = (function () {
|
|||||||
async function sendTx(data, valueWei) {
|
async function sendTx(data, valueWei) {
|
||||||
const c = await IAP.getConfig();
|
const c = await IAP.getConfig();
|
||||||
const addr = await connect();
|
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 };
|
const tx = { from: addr, to: c.contract, data };
|
||||||
if (valueWei) tx.value = '0x' + BigInt(valueWei).toString(16);
|
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] });
|
return eth().request({ method: 'eth_sendTransaction', params: [tx] });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -439,7 +439,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260908c"></script>
|
<script src="/assets/common.js?v=20260908c"></script>
|
||||||
<script src="/assets/wallet.js?v=20260908m"></script>
|
<script src="/assets/wallet.js?v=20260908n"></script>
|
||||||
<script src="/assets/home.js?v=20260906m"></script>
|
<script src="/assets/home.js?v=20260906m"></script>
|
||||||
<script src="/assets/chat.js?v=20260906m"></script>
|
<script src="/assets/chat.js?v=20260906m"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+7
-8
@@ -5,7 +5,7 @@
|
|||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="icon" type="image/png" href="/logo-icon.png">
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260908e">
|
<link rel="stylesheet" href="/assets/site.css?v=20260908f">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
@@ -140,12 +140,6 @@
|
|||||||
<span class="bo-cap">Team</span>
|
<span class="bo-cap">Team</span>
|
||||||
<a href="#" id="chatMenuBtn" class="bo-chat"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:-2px;margin-right:7px"><path d="M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9l-4 4V6a1 1 0 0 1 1-1z"/></svg>Messages<span class="pill" id="chatNavBadge" hidden></span></a>
|
<a href="#" id="chatMenuBtn" class="bo-chat"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:-2px;margin-right:7px"><path d="M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H9l-4 4V6a1 1 0 0 1 1-1z"/></svg>Messages<span class="pill" id="chatNavBadge" hidden></span></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="bo-links">
|
|
||||||
<span class="bo-cap">Site</span>
|
|
||||||
<a href="/">Ad packages</a>
|
|
||||||
<a href="/ledger">Live ledger</a>
|
|
||||||
<a href="/contract">The contract</a>
|
|
||||||
</div>
|
|
||||||
<div class="bo-links">
|
<div class="bo-links">
|
||||||
<span class="bo-cap">Legal</span>
|
<span class="bo-cap">Legal</span>
|
||||||
<a href="/terms" target="_blank" rel="noopener">Terms</a>
|
<a href="/terms" target="_blank" rel="noopener">Terms</a>
|
||||||
@@ -673,6 +667,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
<footer class="bo-pagefoot">
|
||||||
|
<a href="/">Ad packages</a>
|
||||||
|
<a href="/ledger">Live ledger</a>
|
||||||
|
<a href="/contract">The contract</a>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -696,7 +695,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/assets/common.js?v=20260908c"></script>
|
<script src="/assets/common.js?v=20260908c"></script>
|
||||||
<script src="/assets/wallet.js?v=20260908m"></script>
|
<script src="/assets/wallet.js?v=20260908n"></script>
|
||||||
<script src="/assets/my.js?v=20260908l"></script>
|
<script src="/assets/my.js?v=20260908l"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -503,6 +503,11 @@ const server = http.createServer(async (req, res) => {
|
|||||||
await auth.logout(req);
|
await auth.logout(req);
|
||||||
return json(res, 200, { ok: true }, { 'Set-Cookie': auth.clearCookie() });
|
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') {
|
if (p === '/api/me' && req.method === 'GET') {
|
||||||
const s = await auth.fromRequest(req);
|
const s = await auth.fromRequest(req);
|
||||||
if (!s) return json(res, 200, { signedIn: false });
|
if (!s) return json(res, 200, { signedIn: false });
|
||||||
|
|||||||
Reference in New Issue
Block a user