Fix credits showing 0 + purchase confirmation email

- 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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 10:26:53 -05:00
parent 09804cda22
commit 8958a9d145
2 changed files with 24 additions and 5 deletions
+11 -4
View File
@@ -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');
}
+13 -1
View File
@@ -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.');
}