diff --git a/ads.js b/ads.js index 3b96e9d..224f4a0 100644 Binary files a/ads.js and b/ads.js differ diff --git a/burner.js b/burner.js index ce55089..2ef3565 100644 --- a/burner.js +++ b/burner.js @@ -10,7 +10,18 @@ const INFLIGHT_FILE = () => path.join(process.env.DATA_DIR || path.join(__dirnam function loadInflight() { try { return JSON.parse(fs.readFileSync(INFLIGHT_FILE(), 'utf8')); } catch (e) { return {}; } } function saveInflight(o) { try { fs.writeFileSync(INFLIGHT_FILE(), JSON.stringify(o)); } catch (e) {} } -let chain = null, ads = null; +let chain = null, ads = null, accounts = null; +// another of the same account's positions that can cover this burn on-chain (credits are pooled per account) +async function fundedAlternative(b) { + if (!accounts) return null; + const owner = await ads.burnOwner(b.ref); if (!owner) return null; + const acct = await accounts.byEmail(owner); if (!acct) return null; + const ids = [acct.memberId, ...(await accounts.positions(owner)).map(p => p.memberId)].filter(id => id && id !== b.memberId); + for (const id of [...new Set(ids)]) { + try { const bal = await chain.creditBalance(id, 0); const held = await ads.unburnedFor(id); if (bal - held >= b.amount) return id; } catch (e) {} + } + return null; +} const ABI = ['function consume(uint32 memberId_, uint8 creditType, uint256 amount, bytes32 campaignRef)', 'function engineSigner() view returns (address)']; const state = { enabled: false, address: null, signer: null, balanceWei: '0', lastRun: 0, lastError: null, burned: 0, lastTx: null, mismatch: false, skipped: {} }; let running = false; @@ -94,7 +105,15 @@ async function tick() { // dry-run first: a revert here (usually "Insufficient credits", the member's on-chain // balance is below what the engine metered) costs no gas and is left for the admin try { await ctr.consume.staticCall(Number(b.memberId), 0, BigInt(b.amount), refToBytes32(b.id)); } - catch (e) { state.skipped[b.id] = String(e.reason || e.shortMessage || e.message).slice(0, 120); continue; } + catch (e) { + const why = String(e.reason || e.shortMessage || e.message).slice(0, 120); + // the pinned position is dry: settle from another funded position on the same account + const alt = /insufficient credits/i.test(why) ? await fundedAlternative(b) : null; + if (!alt) { state.skipped[b.id] = why + (alt === null && /insufficient credits/i.test(why) ? ' (no funded position on the account)' : ''); continue; } + try { await ctr.consume.staticCall(Number(alt), 0, BigInt(b.amount), refToBytes32(b.id)); } + catch (e2) { state.skipped[b.id] = String(e2.reason || e2.shortMessage || e2.message).slice(0, 120); continue; } + await ads.reassignBurn(b.id, alt); b.memberId = alt; delete state.skipped[b.id]; + } const g = await chain.suggestedFees(); const overrides = { gasLimit: 120000n, maxPriorityFeePerGas: BigInt(g.maxPriorityFeePerGas), maxFeePerGas: BigInt(g.maxFeePerGas) }; state.inflight[b.id] = Date.now(); saveInflight(state.inflight); @@ -115,5 +134,5 @@ async function tick() { return { burned }; } function status() { return Object.assign({}, state, { hasEthers: !!ethers, keyPresent: !!keyHex() }); } -function init(opts) { chain = opts.chain; ads = opts.ads; setup().catch(e => { state.lastError = e.message; }); } +function init(opts) { chain = opts.chain; ads = opts.ads; accounts = opts.accounts || null; setup().catch(e => { state.lastError = e.message; }); } module.exports = { init, tick, status }; diff --git a/server.js b/server.js index 32a4759..d78d6b5 100644 --- a/server.js +++ b/server.js @@ -397,7 +397,7 @@ async function boot() { geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message)); setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily setInterval(() => tank.sweep().catch(e => console.error('tank sweep', e.message)), 60 * 60 * 1000); // adoptions past their 7-day window - burner.init({ chain, ads }); + burner.init({ chain, ads, accounts }); setTimeout(() => coach.nudgeTick().catch(e => console.error('coach', e.message)), 90 * 1000); setInterval(() => coach.nudgeTick().catch(e => console.error('coach', e.message)), 60 * 60 * 1000); setTimeout(() => burner.tick().catch(e => console.error('burner', e.message)), 45 * 1000);