From 8958a9d145e9e12b764e694570ea6b2583d43cca Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 8 Sep 2026 10:26:53 -0500 Subject: [PATCH] Fix credits showing 0 + purchase confirmation email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chain.rpc(): retry the RPC pool 3 rounds with backoff. Public Amoy nodes routinely return transient "Temporary internal error. Please retry" on eth_call; a single miss was silently surfacing as 0 credits / chainReadError on the dashboard even when the buy succeeded on-chain. - emailOnEvent: send a purchase confirmation on the Purchase event — credits added, new ad-credit balance, POL paid, and a link to view the tx on the explorer. Fires server-side for every wallet and the Mini App. - Added a third Amoy RPC (tenderly) to the volume config for read redundancy. Co-Authored-By: Claude Opus 4.8 --- chain.js | 15 +++++++++++---- server.js | 14 +++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/chain.js b/chain.js index 468a9dc..39d24e7 100644 --- a/chain.js +++ b/chain.js @@ -89,10 +89,17 @@ function rpcOnce(url, method, params) { async function rpc(method, params) { const urls = getConfig().rpcs; let last; - for (let i = 0; i < urls.length; i++) { - const url = urls[(rpcIdx + i) % urls.length]; - try { const r = await rpcOnce(url, method, params); rpcIdx = (rpcIdx + i) % urls.length; return r; } - catch (e) { last = e; } + // 3 rounds over the RPC pool with backoff — the public Amoy nodes routinely + // return transient "Temporary internal error. Please retry" on eth_call, and a + // single miss was silently surfacing as 0 credits / chainReadError on the + // dashboard. Retry so a real read isn't lost to a momentary node hiccup. + for (let round = 0; round < 3; round++) { + for (let i = 0; i < urls.length; i++) { + const url = urls[(rpcIdx + i) % urls.length]; + try { const r = await rpcOnce(url, method, params); rpcIdx = (rpcIdx + i) % urls.length; return r; } + catch (e) { last = e; } + } + if (round < 2) await new Promise(r => setTimeout(r, 400 * (round + 1))); } throw last || new Error('all RPCs failed'); } diff --git a/server.js b/server.js index e477aa6..cca59a8 100644 --- a/server.js +++ b/server.js @@ -277,7 +277,19 @@ async function emailOnEvent(ev) { const a = await accounts.byMemberId(memberId); if (a && a.email) mailer.send(a.email, subject, body + '\n\nSee it on the live ledger: https://instantadpay.com/ledger\n\nInstantAdPay').catch(() => {}); }; - if (ev.type === 'TierPaid') await notify(ev.recipientId, 'You just got paid on InstantAdPay', 'A level-' + ev.tier + ' payout of ' + weiToPol(ev.amountWei) + ' POL just landed in your wallet.'); + if (ev.type === 'Purchase') { + const cc = chain.getConfig(); + const txUrl = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://amoy.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', + 'Your purchase is complete and settled on-chain.\n\n' + + 'Ad credits added: ' + ev.creditAmount + '\n' + + 'Your ad-credit balance is now: ' + bal + '\n' + + 'Amount paid: ' + weiToPol(ev.paidWei) + ' POL\n\n' + + 'View your transaction on the blockchain:\n' + txUrl); + } + else if (ev.type === 'TierPaid') await notify(ev.recipientId, 'You just got paid on InstantAdPay', 'A level-' + ev.tier + ' payout of ' + weiToPol(ev.amountWei) + ' POL just landed in your wallet.'); else if (ev.type === 'AwardPaid') await notify(ev.toId, 'You just got paid on InstantAdPay', weiToPol(ev.amountWei) + ' POL just landed in your wallet.'); else if (ev.type === 'PassedUp') await notify(ev.skippedId, 'A payout passed you by on InstantAdPay', 'A level-' + ev.tier + ' payout passed you by because you were not qualified yet. Get qualified so you catch the next one.'); }