Burner: on-chain ref = burn id; recognise already-mined burns from CreditsConsumed and hold in-flight sends, so a lost RPC response can never double-burn
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,14 @@ async function setup() {
|
|||||||
} catch (e) { state.lastError = 'engineSigner read: ' + e.message; }
|
} catch (e) { state.lastError = 'engineSigner read: ' + e.message; }
|
||||||
}
|
}
|
||||||
function refToBytes32(ref) { return ethers.zeroPadBytes(ethers.toUtf8Bytes(String(ref || '').slice(0, 32)), 32); }
|
function refToBytes32(ref) { return ethers.zeroPadBytes(ethers.toUtf8Bytes(String(ref || '').slice(0, 32)), 32); }
|
||||||
|
// the on-chain ref IS the burn id, so a burn that already mined can always be recognised
|
||||||
|
// from its CreditsConsumed event, even when the RPC lost the response (a lost response
|
||||||
|
// double-burned member #5's campaign 30 on 2026-09-10)
|
||||||
|
function alreadyMined(b) {
|
||||||
|
const want = refToBytes32(b.id).toLowerCase();
|
||||||
|
const hit = chain.recentEvents(1e9).find(e => e.type === 'CreditsConsumed' && e.memberId === Number(b.memberId) && String(e.ref || '').toLowerCase() === want);
|
||||||
|
return hit ? hit.tx : null;
|
||||||
|
}
|
||||||
|
|
||||||
async function tick() {
|
async function tick() {
|
||||||
if (!state.enabled || state.mismatch || running || !ethers) return { burned: 0 };
|
if (!state.enabled || state.mismatch || running || !ethers) return { burned: 0 };
|
||||||
@@ -53,15 +61,20 @@ async function tick() {
|
|||||||
try {
|
try {
|
||||||
// Polygon nodes reject low priority fees and some public RPCs answer fee
|
// Polygon nodes reject low priority fees and some public RPCs answer fee
|
||||||
// queries with 500s: set the fees ourselves from the server's estimate
|
// queries with 500s: set the fees ourselves from the server's estimate
|
||||||
|
const mined = alreadyMined(b);
|
||||||
|
if (mined) { await ads.markBurned(b.id, mined); burned += 1; state.burned += 1; continue; }
|
||||||
|
state.inflight = state.inflight || {};
|
||||||
|
if (state.inflight[b.id] && Date.now() - state.inflight[b.id] < 10 * 60000) continue; // sent recently, answer lost: wait for the event
|
||||||
// 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.ref)); }
|
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) { state.skipped[b.id] = String(e.reason || e.shortMessage || e.message).slice(0, 120); continue; }
|
||||||
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) };
|
||||||
const tx = await ctr.consume(Number(b.memberId), 0, BigInt(b.amount), refToBytes32(b.ref), overrides);
|
state.inflight[b.id] = Date.now();
|
||||||
|
const tx = await ctr.consume(Number(b.memberId), 0, BigInt(b.amount), refToBytes32(b.id), overrides);
|
||||||
const rc = await tx.wait(1);
|
const rc = await tx.wait(1);
|
||||||
if (rc && rc.status === 1) { await ads.markBurned(b.id, tx.hash); burned += 1; state.burned += 1; state.lastTx = tx.hash; state.lastError = null; }
|
if (rc && rc.status === 1) { await ads.markBurned(b.id, tx.hash); delete state.inflight[b.id]; burned += 1; state.burned += 1; state.lastTx = tx.hash; state.lastError = null; }
|
||||||
else { state.lastError = 'consume reverted for burn ' + b.id; break; }
|
else { state.lastError = 'consume reverted for burn ' + b.id; break; }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state.lastError = 'burn ' + b.id + ': ' + String(e.shortMessage || e.message).slice(0, 160);
|
state.lastError = 'burn ' + b.id + ': ' + String(e.shortMessage || e.message).slice(0, 160);
|
||||||
|
|||||||
Reference in New Issue
Block a user