Faucet: settle sent drips from their receipts on every tick; a wait timeout or restart never strands or re-sends a drip
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+23
-5
@@ -4,8 +4,10 @@
|
|||||||
// HUNT_RPC e.g. https://polygon-amoy-bor-rpc.publicnode.com (Amoy) or a mainnet RPC
|
// HUNT_RPC e.g. https://polygon-amoy-bor-rpc.publicnode.com (Amoy) or a mainnet RPC
|
||||||
// HUNT_CHAIN_ID 80002 (Amoy) or 137 (mainnet)
|
// 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
|
// Every send is one plain POL transfer, recorded on the ledger with its tx hash. A drip whose hash
|
||||||
// the entry 'failed' with the reason and never retries by itself (a human looks). The balance is
|
// is on the ledger is never sent again: it is settled from its receipt on a later tick if the wait
|
||||||
|
// timed out or the process restarted. A send that produced no hash 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.
|
// read on every tick; below lowBalancePol the alert fires once per day.
|
||||||
'use strict';
|
'use strict';
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
@@ -24,11 +26,24 @@ function init() {
|
|||||||
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)); }
|
||||||
|
|
||||||
|
// a drip is settled from its receipt: paid on status 1, failed on a revert
|
||||||
|
async function settle(p, rc, notify) {
|
||||||
|
const ok = rc && rc.status === 1;
|
||||||
|
rewards.mark(p.id, { status: ok ? 'paid' : 'failed', paidAt: Date.now(), error: ok ? null : 'reverted' });
|
||||||
|
if (ok && notify) await notify('paid', Object.assign({}, p, { tx: p.tx }));
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
async function tick(notify) {
|
async function tick(notify) {
|
||||||
if (!wallet || ticking) return { paid: 0 };
|
if (!wallet || ticking) return { paid: 0 };
|
||||||
ticking = true;
|
ticking = true;
|
||||||
let paid = 0;
|
let paid = 0;
|
||||||
try {
|
try {
|
||||||
|
// drips sent but never settled here (a restart mid-wait, a receipt that lagged past the wait):
|
||||||
|
// ask the chain, never re-send
|
||||||
|
for (const p of store.read('payouts', []).filter(x => x.status === 'sent' && x.tx)) {
|
||||||
|
try { const rc = await provider.getTransactionReceipt(p.tx); if (rc && (await settle(p, rc, notify))) paid++; } catch (e) {}
|
||||||
|
}
|
||||||
const due = rewards.payable();
|
const due = rewards.payable();
|
||||||
// a dry faucet is not a failed drip: check the balance first, leave the drips due, alert once
|
// 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) {}
|
let bal0 = null; try { bal0 = await balance(); } catch (e) {}
|
||||||
@@ -42,12 +57,15 @@ async function tick(notify) {
|
|||||||
try {
|
try {
|
||||||
const tx = await wallet.sendTransaction({ to: p.wallet, value: ethers.parseEther(String(p.pol)) });
|
const tx = await wallet.sendTransaction({ to: p.wallet, value: ethers.parseEther(String(p.pol)) });
|
||||||
rewards.mark(p.id, { status: 'sent', tx: tx.hash });
|
rewards.mark(p.id, { status: 'sent', tx: tx.hash });
|
||||||
const rc = await tx.wait(1);
|
let rc = null;
|
||||||
rewards.mark(p.id, { status: rc && rc.status === 1 ? 'paid' : 'failed', paidAt: Date.now(), error: rc && rc.status === 1 ? null : 'reverted' });
|
try { rc = await tx.wait(1, 90000); } catch (e) { if (e && e.code === 'TIMEOUT') continue; throw e; } // still pending: stays 'sent', settled next tick
|
||||||
if (rc && rc.status === 1) { paid++; if (bal0 != null) bal0 -= p.pol; if (notify) await notify('paid', Object.assign({}, p, { tx: tx.hash })); }
|
if (await settle(Object.assign({}, p, { tx: tx.hash }), rc, notify)) { paid++; if (bal0 != null) bal0 -= p.pol; }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = String(e.message || 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
|
if (/insufficient funds/i.test(msg)) { rewards.mark(p.id, { status: 'due', error: null }); break; } // dry: leave it due, stop this pass
|
||||||
|
// 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);
|
||||||
|
if (cur && cur.status === 'sent' && cur.tx) continue;
|
||||||
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) }));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user