fd72ea1b84
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
68 lines
4.1 KiB
JavaScript
68 lines
4.1 KiB
JavaScript
// The faucet: pays due drips from the dedicated hot wallet. Amoy first, mainnet when Marty flips it.
|
|
//
|
|
// HUNT_WALLET_KEY the hot wallet's private key (env only; never on disk in the repo or volume)
|
|
// HUNT_RPC e.g. https://polygon-amoy-bor-rpc.publicnode.com (Amoy) or a mainnet RPC
|
|
// HUNT_CHAIN_ID 80002 (Amoy) or 137 (mainnet)
|
|
//
|
|
// Every send is one plain POL transfer, recorded on the ledger with its tx hash. A failure marks
|
|
// the entry 'failed' with the reason and never retries by itself (a human looks). The balance is
|
|
// read on every tick; below lowBalancePol the alert fires once per day.
|
|
'use strict';
|
|
const { ethers } = require('ethers');
|
|
const rewards = require('./rewards');
|
|
const store = require('./store');
|
|
|
|
let provider = null, wallet = null, ticking = false;
|
|
|
|
function enabled() { return !!(process.env.HUNT_WALLET_KEY && process.env.HUNT_RPC); }
|
|
function init() {
|
|
if (!enabled()) return false;
|
|
provider = new ethers.JsonRpcProvider(process.env.HUNT_RPC, Number(process.env.HUNT_CHAIN_ID) || undefined);
|
|
wallet = new ethers.Wallet(process.env.HUNT_WALLET_KEY.trim(), provider);
|
|
return true;
|
|
}
|
|
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 tick(notify) {
|
|
if (!wallet || ticking) return { paid: 0 };
|
|
ticking = true;
|
|
let paid = 0;
|
|
try {
|
|
const due = rewards.payable();
|
|
// a dry faucet is not a failed drip: check the balance first, leave the drips due, alert once
|
|
let bal0 = null; try { bal0 = await balance(); } catch (e) {}
|
|
if (bal0 != null && due.length) {
|
|
const need = due.reduce((n, p) => n + p.pol, 0) + 0.01;
|
|
if (bal0 < due[0].pol + 0.005) { if (notify) { const st = store.read('faucet-state', {}); const day = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Chicago' }); if (st.dryAlertDay !== day) { st.dryAlertDay = day; store.write('faucet-state', st); await notify('low', { balance: bal0, threshold: need, address: wallet.address }); } } ticking = false; return { paid: 0, waiting: due.length, balance: bal0 }; }
|
|
}
|
|
for (const p of due) {
|
|
if (bal0 != null && bal0 < p.pol + 0.005) break; // pay what the balance covers, leave the rest due
|
|
if (!p.wallet || !/^0x[a-f0-9]{40}$/i.test(p.wallet)) { rewards.mark(p.id, { status: 'failed', error: 'no wallet on the account' }); continue; }
|
|
try {
|
|
const tx = await wallet.sendTransaction({ to: p.wallet, value: ethers.parseEther(String(p.pol)) });
|
|
rewards.mark(p.id, { status: 'sent', tx: tx.hash });
|
|
const rc = await tx.wait(1);
|
|
rewards.mark(p.id, { status: rc && rc.status === 1 ? 'paid' : 'failed', paidAt: Date.now(), error: rc && rc.status === 1 ? null : 'reverted' });
|
|
if (rc && rc.status === 1) { paid++; if (bal0 != null) bal0 -= p.pol; if (notify) await notify('paid', Object.assign({}, p, { tx: tx.hash })); }
|
|
} catch (e) {
|
|
const msg = String(e.message || e);
|
|
if (/insufficient funds/i.test(msg)) { rewards.mark(p.id, { status: 'due', error: null }); break; } // dry: leave it due, stop this pass
|
|
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) }));
|
|
}
|
|
}
|
|
// low-balance alert, once a day
|
|
const bal = await balance();
|
|
if (bal != null) {
|
|
const s = rewards.settings(); const st = store.read('faucet-state', {});
|
|
const day = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Chicago' });
|
|
if (bal < Number(s.lowBalancePol) && st.lowAlertDay !== day) { st.lowAlertDay = day; store.write('faucet-state', st); if (notify) await notify('low', { balance: bal, threshold: s.lowBalancePol, address: wallet.address }); }
|
|
store.update('faucet-state', {}, x => Object.assign(x, { balance: bal, checkedAt: Date.now() }));
|
|
}
|
|
} finally { ticking = false; }
|
|
return { paid };
|
|
}
|
|
|
|
module.exports = { enabled, init, address, balance, tick };
|