From 873a9e5e4a4ddd7668f41715ac5100e640aab8b8 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 8 Sep 2026 11:20:47 -0500 Subject: [PATCH] Mainnet indexer fix + retire testnet copy - chain.js: derive the chain tip as the MAX height across the RPC pool and scan getLogs against that same synced node. Load-balanced public RPCs (publicnode) intermittently answer eth_blockNumber from a replica lagging thousands of blocks behind, which stalled the mainnet scan (latest < lastBlock, 0 events) and could skip freshly-mined events. This unblocks the live ledger + the purchase-confirmation email (both driven by the event scan). - disclaimer: replace the "rehearsal/testnet" section with a live-on-Polygon real-money notice (it's live now). - Point the dead Amoy fallbacks (purchase email tx link, contract-page source link) at Polygon mainnet / the verified mainnet contract. Co-Authored-By: Claude Opus 4.8 --- chain.js | 27 ++++++++++++++++++++++++--- public/assets/contract.js | 4 ++-- public/contract.html | 2 +- public/disclaimer.html | 4 ++-- server.js | 2 +- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/chain.js b/chain.js index a394b01..11a72cf 100644 --- a/chain.js +++ b/chain.js @@ -189,15 +189,36 @@ function saveState() { fs.renameSync(tmp, STATE_FILE); } catch (e) { console.error('chain state save failed', e.message); } } +// Most-synced tip across the RPC pool. Public load-balanced nodes (publicnode) +// sometimes answer eth_blockNumber from a replica lagging thousands of blocks +// behind — which stalls the scan (latest < lastBlock) and, worse, could let a +// getLogs from a lagging node skip freshly-mined events for good. Take the MAX +// height and remember which node reported it, so we scan against a node we know +// is synced to that height. +async function bestTip() { + const urls = getConfig().rpcs; + let best = { block: 0, url: urls[0] }; + await Promise.all(urls.map(async u => { + try { const b = parseInt(await rpcOnce(u, 'eth_blockNumber', []), 16); + if (b > best.block) best = { block: b, url: u }; } catch (e) {} + })); + return best; +} async function tail() { if (busy) return; busy = true; try { - const latest = parseInt(await rpc('eth_blockNumber', []), 16); + const tip = await bestTip(); + const latest = tip.block; while (state.lastBlock < latest) { const from = state.lastBlock + 1; const to = Math.min(from + CHUNK - 1, latest); - const logs = await rpc('eth_getLogs', [{ address: getConfig().contract, - fromBlock: '0x' + from.toString(16), toBlock: '0x' + to.toString(16) }]); + // scan against the node we confirmed is synced to `latest`; fall back to + // the rotating pool only if that specific node errors on this range. + let logs; + try { logs = await rpcOnce(tip.url, 'eth_getLogs', [{ address: getConfig().contract, + fromBlock: '0x' + from.toString(16), toBlock: '0x' + to.toString(16) }]); } + catch (e) { logs = await rpc('eth_getLogs', [{ address: getConfig().contract, + fromBlock: '0x' + from.toString(16), toBlock: '0x' + to.toString(16) }]); } for (const lg of logs) { const ev = decodeLog(lg); if (!ev) continue; diff --git a/public/assets/contract.js b/public/assets/contract.js index 2993aac..c984a47 100644 --- a/public/assets/contract.js +++ b/public/assets/contract.js @@ -12,8 +12,8 @@ $('lnkSource').href = src; $('lnkSource2').href = src; } else { - // private rehearsal chain: point at the audited Amoy verification instead - const src = 'https://repo.sourcify.dev/contracts/full_match/80002/0xaF47EC401A9B0D570a553b1A1c95D6079083636e/'; + // fallback if explorer unset: point straight at the Sourcify-verified source + const src = 'https://repo.sourcify.dev/contracts/full_match/137/0xBE1ECA72AFF47d13D8E907e523F55eB9e2d365E0/'; $('lnkExplorer').href = src; $('lnkSource').href = src; $('lnkSource2').href = src; diff --git a/public/contract.html b/public/contract.html index 6720abb..8dd465c 100644 --- a/public/contract.html +++ b/public/contract.html @@ -142,7 +142,7 @@ - + diff --git a/public/disclaimer.html b/public/disclaimer.html index 68343a1..2364db7 100644 --- a/public/disclaimer.html +++ b/public/disclaimer.html @@ -18,8 +18,8 @@

Nothing on the Platform is investment, financial, tax, or legal advice. Do your own research and consult a professional before spending money.

Crypto risk

Purchases settle on a public blockchain using a volatile network token. On-chain transactions are final and irreversible: there are no refunds, chargebacks, or reversals. You are responsible for your wallet, your keys, network fees, and the tax treatment of your activity.

-

Rehearsal / testnet

-

During any rehearsal or testnet phase, the Platform runs on test tokens with no monetary value. Balances, purchases, and payouts during that phase are for testing only and do not carry over to a live launch.

+

Live on Polygon

+

The Platform is live on the Polygon mainnet. Purchases and payouts use POL, a real cryptocurrency with real monetary value. Every transaction is real and final — this is not a simulation or test environment.

Advertising

Ads are member-created and auto-approved for speed. InstantAdPay does not endorse and is not responsible for advertised products, sites, or claims. Use your judgment, and report anything broken or inappropriate.

Your responsibility

diff --git a/server.js b/server.js index 3c283e9..7ea5f61 100644 --- a/server.js +++ b/server.js @@ -279,7 +279,7 @@ async function emailOnEvent(ev) { }; if (ev.type === 'Purchase') { const cc = chain.getConfig(); - const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://amoy.polygonscan.com') + '/tx/' + ev.tx; + const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx; let bal = ev.creditAmount; try { bal = await chain.creditBalance(ev.buyerId, ev.creditType); } catch (e) {} await notify(ev.buyerId, 'Your InstantAdPay purchase is confirmed',