Rehearsal faucet: self-serve test-POL so members can actually buy

Fresh wallets hold 0 POL on the rehearsal chain, so nobody could complete a buy.
- Oracle: the mock POL/USD feed had gone ~49h stale (quoteWei reverted "Stale
  oracle" -> catalog costWei null -> packages showed "paused"). Refreshed it and
  installed a 15-min heartbeat cron so it stays fresh; quotes now return.
- New /api/my/faucet (rehearsal-only, rate-limited) tops a connected wallet up to
  10 test-POL via anvil_setBalance (no key). "Get test POL" card in the Wallet
  pane wires it. Members can now fund, buy, and see payouts move on the ledger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-07 07:02:48 -05:00
parent 85a6ac5bb0
commit 204aa6a723
3 changed files with 43 additions and 4 deletions
+22
View File
@@ -36,6 +36,7 @@ fs.mkdirSync(UPLOADS_DIR, { recursive: true });
const uploadCounts = new Map(); // email:day -> uploads today
const gauntletTokens = new Map(); // email -> welcome-tour token (server-clock dwell floor)
const videoTokens = new Map(); // email -> watch-to-earn video token (server-clock watch floor)
const faucetHits = new Map(); // address -> last faucet ts (rehearsal test-POL faucet rate limit)
const visitTokens = new Map(); // email -> verified-visit token (dwell + captcha floor)
// walk the referral chain upward via sponsorRef (code/username/member id)
async function uplineSlides(email, depth = 3) {
@@ -681,6 +682,27 @@ const server = http.createServer(async (req, res) => {
if (s && s.email) accounts.touchSeen(s.email).catch(() => {});
return json(res, 200, { ok: true });
}
// -- rehearsal test-POL faucet: top a connected wallet up to 10 test-POL
// (anvil_setBalance, no key needed). Rehearsal-only, rate-limited.
if (p === '/api/my/faucet' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s) return json(res, 401, { error: 'Sign in first.' });
if (!siteConfig().rehearsal) return json(res, 400, { error: 'The faucet is only open during the rehearsal.' });
const b = await readBody(req);
const addr = String(b.address || '').trim().toLowerCase();
if (!/^0x[0-9a-f]{40}$/.test(addr)) return json(res, 400, { error: 'Connect your wallet first.' });
const now = Date.now(), last = faucetHits.get(addr) || 0;
if (now - last < 120000) return json(res, 429, { error: 'Just topped up. Give it a minute, then try again.' });
try {
const balHex = await chain.rpc('eth_getBalance', [addr, 'latest']);
const bal = BigInt(balHex || '0x0');
const target = 10n * (10n ** 18n); // 10 test-POL
if (bal < 5n * (10n ** 18n)) await chain.rpc('anvil_setBalance', [addr, '0x' + target.toString(16)]);
faucetHits.set(addr, now);
const nb = await chain.rpc('eth_getBalance', [addr, 'latest']);
return json(res, 200, { ok: true, balanceWei: BigInt(nb || '0x0').toString() });
} catch (e) { return json(res, 502, { error: 'Faucet is unavailable right now.' }); }
}
if (p === '/api/my/chat/send' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });