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:
@@ -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 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) {} }
|
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 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: {} };
|
const state = { enabled: false, address: null, signer: null, balanceWei: '0', lastRun: 0, lastError: null, burned: 0, lastTx: null, mismatch: false, skipped: {} };
|
||||||
let running = false;
|
let running = false;
|
||||||
@@ -94,7 +105,15 @@ async function tick() {
|
|||||||
// dry-run first: a revert here (usually "Insufficient credits", the member's on-chain
|
// 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
|
// 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)); }
|
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 g = await chain.suggestedFees();
|
||||||
const overrides = { gasLimit: 120000n, maxPriorityFeePerGas: BigInt(g.maxPriorityFeePerGas), maxFeePerGas: BigInt(g.maxFeePerGas) };
|
const overrides = { gasLimit: 120000n, maxPriorityFeePerGas: BigInt(g.maxPriorityFeePerGas), maxFeePerGas: BigInt(g.maxFeePerGas) };
|
||||||
state.inflight[b.id] = Date.now(); saveInflight(state.inflight);
|
state.inflight[b.id] = Date.now(); saveInflight(state.inflight);
|
||||||
@@ -115,5 +134,5 @@ async function tick() {
|
|||||||
return { burned };
|
return { burned };
|
||||||
}
|
}
|
||||||
function status() { return Object.assign({}, state, { hasEthers: !!ethers, keyPresent: !!keyHex() }); }
|
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 };
|
module.exports = { init, tick, status };
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ async function boot() {
|
|||||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
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(() => 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
|
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);
|
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);
|
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);
|
setTimeout(() => burner.tick().catch(e => console.error('burner', e.message)), 45 * 1000);
|
||||||
|
|||||||
Reference in New Issue
Block a user