Burner: when a campaign's pinned position is dry on-chain, settle the spend from another funded position on the same account (credits are pooled per account)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 12:49:16 -05:00
parent 92e7304882
commit 66b3c83d3d
3 changed files with 23 additions and 4 deletions
+22 -3
View File
@@ -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 };