No double posts: badge and find posts reserve an in-flight key the sweeps skip (bikereo2's First Find posted twice, 16 s race); faucet keeps a drip due on transient node errors and rotates through HUNT_RPC (comma list)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -19,10 +19,18 @@ let provider = null, wallet = null, ticking = false;
|
|||||||
function enabled() { return !!(process.env.HUNT_WALLET_KEY && process.env.HUNT_RPC); }
|
function enabled() { return !!(process.env.HUNT_WALLET_KEY && process.env.HUNT_RPC); }
|
||||||
function init() {
|
function init() {
|
||||||
if (!enabled()) return false;
|
if (!enabled()) return false;
|
||||||
provider = new ethers.JsonRpcProvider(process.env.HUNT_RPC, Number(process.env.HUNT_CHAIN_ID) || undefined);
|
rpcs = String(process.env.HUNT_RPC).split(',').map(s => s.trim()).filter(Boolean); rpcAt = 0;
|
||||||
wallet = new ethers.Wallet(process.env.HUNT_WALLET_KEY.trim(), provider);
|
connect();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
let rpcs = [], rpcAt = 0;
|
||||||
|
function connect() {
|
||||||
|
provider = new ethers.JsonRpcProvider(rpcs[rpcAt % rpcs.length], Number(process.env.HUNT_CHAIN_ID) || undefined);
|
||||||
|
wallet = new ethers.Wallet(process.env.HUNT_WALLET_KEY.trim(), provider);
|
||||||
|
}
|
||||||
|
// a node hiccup is not a failed drip: 5xx, timeouts, connection resets, rate limits, missing responses
|
||||||
|
const TRANSIENT = /server response 5\d\d|timeout|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|429|rate limit|missing response|could not detect network|bad response|SERVER_ERROR|NETWORK_ERROR/i;
|
||||||
|
function rotate(reason) { if (rpcs.length > 1) { rpcAt = (rpcAt + 1) % rpcs.length; connect(); console.warn('faucet: RPC rotated to', rpcs[rpcAt % rpcs.length], 'after', reason.slice(0, 80)); } }
|
||||||
function address() { return wallet ? wallet.address : null; }
|
function address() { return wallet ? wallet.address : null; }
|
||||||
async function balance() { if (!provider || !wallet) return null; const b = await provider.getBalance(wallet.address); return Number(ethers.formatEther(b)); }
|
async function balance() { if (!provider || !wallet) return null; const b = await provider.getBalance(wallet.address); return Number(ethers.formatEther(b)); }
|
||||||
|
|
||||||
@@ -66,6 +74,7 @@ async function tick(notify) {
|
|||||||
// the send itself failed only if no hash was recorded; a hash means the chain has it, leave it 'sent'
|
// the send itself failed only if no hash was recorded; a hash means the chain has it, leave it 'sent'
|
||||||
const cur = store.read('payouts', []).find(x => x.id === p.id);
|
const cur = store.read('payouts', []).find(x => x.id === p.id);
|
||||||
if (cur && cur.status === 'sent' && cur.tx) continue;
|
if (cur && cur.status === 'sent' && cur.tx) continue;
|
||||||
|
if (TRANSIENT.test(msg)) { rewards.mark(p.id, { status: 'due', error: 'node: ' + msg.slice(0, 120) }); console.warn('faucet: transient, left due:', p.id, msg.slice(0, 120)); rotate(msg); break; } // next tick, next node
|
||||||
rewards.mark(p.id, { status: 'failed', error: msg.slice(0, 200) });
|
rewards.mark(p.id, { status: 'failed', error: msg.slice(0, 200) });
|
||||||
if (notify) await notify('failed', Object.assign({}, p, { error: String(e.message || e).slice(0, 200) }));
|
if (notify) await notify('failed', Object.assign({}, p, { error: String(e.message || e).slice(0, 200) }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,14 +58,23 @@ async function telegramPhoto(file, caption, general) {
|
|||||||
} catch (e) { return false; }
|
} catch (e) { return false; }
|
||||||
}
|
}
|
||||||
const fmt = n => Number(n).toLocaleString('en-US', { maximumFractionDigits: 4 });
|
const fmt = n => Number(n).toLocaleString('en-US', { maximumFractionDigits: 4 });
|
||||||
|
// keys being posted right now, so the claim path and the sweep never post the same thing twice
|
||||||
|
const postingNow = new Set();
|
||||||
async function notify(kind, p) {
|
async function notify(kind, p) {
|
||||||
if (kind === 'paid') { const ok = await telegram('\u{1F3AF} <b>PolHunter</b> · ' + (p.username ? '@' + p.username : '#' + p.memberId) + ' found it on ' + p.site + ' and got <b>' + fmt(p.pol) + ' POL</b> · <a href="' + explorer() + '/tx/' + p.tx + '">verify</a>\n<a href="' + SITE + '">Hunt yours</a>'); if (ok && p.id) rewards.mark(p.id, { posted: Date.now() }); return ok; }
|
if (kind === 'paid' && p && p.id) { const k = 'find:' + p.id; if (postingNow.has(k)) return false; postingNow.add(k); try { return await notifyPaid(p); } finally { postingNow.delete(k); } }
|
||||||
|
if (kind === 'badge' && p && p.me) { const k = 'badge:' + p.me.memberId + ':' + p.id; if (postingNow.has(k)) return false; postingNow.add(k); try { return await notifyBadge(p); } finally { postingNow.delete(k); } }
|
||||||
|
return notifyOther(kind, p);
|
||||||
|
}
|
||||||
|
async function notifyPaid(p) {
|
||||||
|
{ const ok = await telegram('\u{1F3AF} <b>PolHunter</b> · ' + (p.username ? '@' + p.username : '#' + p.memberId) + ' found it on ' + p.site + ' and got <b>' + fmt(p.pol) + ' POL</b> · <a href="' + explorer() + '/tx/' + p.tx + '">verify</a>\n<a href="' + SITE + '">Hunt yours</a>'); if (ok && p.id) rewards.mark(p.id, { posted: Date.now() }); return ok; }
|
||||||
|
}
|
||||||
|
async function notifyOther(kind, p) {
|
||||||
if (kind === 'low') return telegram('⚠️ <b>PolHunter faucet is low</b>: ' + fmt(p.balance) + ' POL left in ' + p.address + ' (alert threshold ' + fmt(p.threshold) + '). Top up from Receiver B.');
|
if (kind === 'low') return telegram('⚠️ <b>PolHunter faucet is low</b>: ' + fmt(p.balance) + ' POL left in ' + p.address + ' (alert threshold ' + fmt(p.threshold) + '). Top up from Receiver B.');
|
||||||
if (kind === 'failed') return telegram('❌ <b>PolHunter</b> · drip to #' + p.memberId + ' failed: ' + p.error);
|
if (kind === 'failed') return telegram('❌ <b>PolHunter</b> · drip to #' + p.memberId + ' failed: ' + p.error);
|
||||||
if (kind === 'badge') { const ok = await postBadge(p); if (ok) store.update('badges-posted', {}, all => { const k = String(p.me.memberId); all[k] = Array.from(new Set([...(all[k] || []), p.id])); return all; }); return ok; }
|
|
||||||
if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} <b>PolHunter weekly prizes</b> for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 <b>' + fmt(w.prizePol) + ' POL</b>').join('\n') + '\n<a href="' + SITE + '/leaders">Leaderboard</a>'); }
|
if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} <b>PolHunter weekly prizes</b> for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 <b>' + fmt(w.prizePol) + ' POL</b>').join('\n') + '\n<a href="' + SITE + '/leaders">Leaderboard</a>'); }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
async function notifyBadge(p) { const ok = await postBadge(p); if (ok) store.update('badges-posted', {}, all => { const k = String(p.me.memberId); all[k] = Array.from(new Set([...(all[k] || []), p.id])); return all; }); return ok; }
|
||||||
async function postBadge(p) {
|
async function postBadge(p) {
|
||||||
{ const b = social.BADGES.find(x => x.id === p.id); if (!b) return false; const who = social.nameOf(p.me); const file = await badge.render(p.id, p.me.username || '#' + p.me.memberId); const cap = '\u{1F3C6} <b>PolHunter</b> \u00b7 <b>' + (p.me.username ? '@' + p.me.username : '#' + p.me.memberId) + '</b> unlocked <b>' + b.name + '</b>: ' + b.why + '\n' + SITE + '/b/' + who + '/' + p.id; if (!file) return telegram(cap); const ok = await telegramPhoto(file, cap); if (String(process.env.HUNT_TG_BADGE_GENERAL || 'on') === 'on') await telegramPhoto(file, cap, true); return ok; }
|
{ const b = social.BADGES.find(x => x.id === p.id); if (!b) return false; const who = social.nameOf(p.me); const file = await badge.render(p.id, p.me.username || '#' + p.me.memberId); const cap = '\u{1F3C6} <b>PolHunter</b> \u00b7 <b>' + (p.me.username ? '@' + p.me.username : '#' + p.me.memberId) + '</b> unlocked <b>' + b.name + '</b>: ' + b.why + '\n' + SITE + '/b/' + who + '/' + p.id; if (!file) return telegram(cap); const ok = await telegramPhoto(file, cap); if (String(process.env.HUNT_TG_BADGE_GENERAL || 'on') === 'on') await telegramPhoto(file, cap, true); return ok; }
|
||||||
return false;
|
return false;
|
||||||
@@ -288,7 +297,7 @@ const POST_SINCE = process.env.HUNT_POST_SINCE ? Date.parse(process.env.HUNT_POS
|
|||||||
let sweeping = false;
|
let sweeping = false;
|
||||||
async function postMissedFinds() {
|
async function postMissedFinds() {
|
||||||
if (sweeping || !outbound()) return 0; sweeping = true; let n = 0;
|
if (sweeping || !outbound()) return 0; sweeping = true; let n = 0;
|
||||||
try { for (const p of rewards.unposted(POST_SINCE, 15)) { if (await notify('paid', p)) n++; else break; } }
|
try { for (const p of rewards.unposted(POST_SINCE, 15)) { if (postingNow.has('find:' + p.id)) continue; if (await notify('paid', p)) n++; else break; } }
|
||||||
catch (e) { console.error('find sweep', e.message); } finally { sweeping = false; }
|
catch (e) { console.error('find sweep', e.message); } finally { sweeping = false; }
|
||||||
if (n) console.log('posted', n, 'missed find(s) to Telegram');
|
if (n) console.log('posted', n, 'missed find(s) to Telegram');
|
||||||
await postMissedBadges().catch(e => console.error('badge sweep', e.message));
|
await postMissedBadges().catch(e => console.error('badge sweep', e.message));
|
||||||
@@ -302,7 +311,7 @@ async function postMissedBadges() {
|
|||||||
const posted = store.read('badges-posted', {}); let n = 0;
|
const posted = store.read('badges-posted', {}); let n = 0;
|
||||||
for (const me of seen.values()) {
|
for (const me of seen.values()) {
|
||||||
const have = new Set(posted[String(me.memberId)] || []);
|
const have = new Set(posted[String(me.memberId)] || []);
|
||||||
for (const b of social.badgesFor(me.memberId)) { if (have.has(b.id)) continue; if (n >= 10) return n; if (await notify('badge', { me, id: b.id })) n++; else return n; }
|
for (const b of social.badgesFor(me.memberId)) { if (have.has(b.id) || postingNow.has('badge:' + me.memberId + ':' + b.id)) continue; if (n >= 10) return n; if (await notify('badge', { me, id: b.id })) n++; else return n; }
|
||||||
}
|
}
|
||||||
if (n) console.log('posted', n, 'missed badge(s) to Telegram');
|
if (n) console.log('posted', n, 'missed badge(s) to Telegram');
|
||||||
return n;
|
return n;
|
||||||
|
|||||||
Reference in New Issue
Block a user