diff --git a/lib/faucet.js b/lib/faucet.js index 8fec9ae..c0e575c 100644 --- a/lib/faucet.js +++ b/lib/faucet.js @@ -30,16 +30,25 @@ async function tick(notify) { 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 (notify) await notify('paid', Object.assign({}, p, { tx: tx.hash })); } + 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) { - rewards.mark(p.id, { status: 'failed', error: String(e.message || e).slice(0, 200) }); + 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) })); } } diff --git a/lib/rewards.js b/lib/rewards.js index 3a7d5a7..dd573a6 100644 --- a/lib/rewards.js +++ b/lib/rewards.js @@ -30,8 +30,10 @@ function paidToday(day) { } // has this member completed this mission already? -function completed(memberId, missionId) { - return store.read('payouts', []).some(p => p.memberId === Number(memberId) && p.missionId === missionId && p.status !== 'failed'); +// by member id, and also by wallet: two accounts sharing a wallet get one drip per mission between them +function completed(memberId, missionId, wallet) { + const w = wallet ? String(wallet).toLowerCase() : null; + return store.read('payouts', []).some(p => p.missionId === missionId && p.status !== 'failed' && (p.memberId === Number(memberId) || (w && p.wallet && String(p.wallet).toLowerCase() === w))); } // record a completion; decide paid-today vs queued diff --git a/public/admin.html b/public/admin.html index ea118b9..9f7b5d5 100644 --- a/public/admin.html +++ b/public/admin.html @@ -5,7 +5,7 @@