Coaching layer + tools: coach your directs (rungs, stalled, one-click nudges), automatic member nudges + weekly sponsor digest, prospects list, per-angle link stats, broadcast templates, Qualified Start calculator, Telegram proof feed, send-failed alerts, admin P&L pane, automatic credit burner (ethers), username lock, home-page comparison, printable checklist, wall link in Promo tools
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// Automatic credit burner: settles pending campaign spend on-chain by calling
|
||||
// consume() from the engine signer. Inert unless ENGINE_KEY (hex private key)
|
||||
// or ENGINE_KEY_FILE is set. Purchased credits only ever go DOWN via this path,
|
||||
// and members never sign or pay gas for it: the engine wallet pays.
|
||||
let ethers = null; try { ethers = require('ethers'); } catch (e) { /* optional dependency */ }
|
||||
const fs = require('fs');
|
||||
|
||||
let chain = null, ads = 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 };
|
||||
let running = false;
|
||||
|
||||
function keyHex() {
|
||||
let k = String(process.env.ENGINE_KEY || '').trim();
|
||||
if (!k && process.env.ENGINE_KEY_FILE) { try { k = fs.readFileSync(process.env.ENGINE_KEY_FILE, 'utf8').trim(); } catch (e) {} }
|
||||
if (!k) return null;
|
||||
if (!k.startsWith('0x')) k = '0x' + k;
|
||||
return /^0x[0-9a-fA-F]{64}$/.test(k) ? k : null;
|
||||
}
|
||||
function provider() {
|
||||
const c = chain.getConfig();
|
||||
const url = (c.rpcs && c.rpcs[0]) || c.rpc;
|
||||
return new ethers.JsonRpcProvider(url, Number(c.chainId));
|
||||
}
|
||||
async function setup() {
|
||||
const k = keyHex();
|
||||
if (!k || !ethers) { state.enabled = false; return; }
|
||||
const w = new ethers.Wallet(k, provider());
|
||||
state.address = w.address; state.enabled = true;
|
||||
try {
|
||||
const ctr = new ethers.Contract(chain.getConfig().contract, ABI, w);
|
||||
const es = await ctr.engineSigner();
|
||||
state.mismatch = String(es).toLowerCase() !== w.address.toLowerCase();
|
||||
if (state.mismatch) console.error('burner: ENGINE_KEY address', w.address, 'is not the contract engineSigner', es, '- burns will revert; disabled');
|
||||
} catch (e) { state.lastError = 'engineSigner read: ' + e.message; }
|
||||
}
|
||||
function refToBytes32(ref) { return ethers.zeroPadBytes(ethers.toUtf8Bytes(String(ref || '').slice(0, 32)), 32); }
|
||||
|
||||
async function tick() {
|
||||
if (!state.enabled || state.mismatch || running || !ethers) return { burned: 0 };
|
||||
running = true;
|
||||
let burned = 0;
|
||||
try {
|
||||
const w = new ethers.Wallet(keyHex(), provider());
|
||||
const ctr = new ethers.Contract(chain.getConfig().contract, ABI, w);
|
||||
state.balanceWei = (await w.provider.getBalance(w.address)).toString();
|
||||
const pending = await ads.pendingBurns();
|
||||
state.lastRun = Date.now();
|
||||
if (BigInt(state.balanceWei) < ethers.parseEther('0.05')) { state.lastError = 'engine wallet low on POL for gas'; return { burned: 0 }; }
|
||||
for (const b of pending.slice(0, 20)) {
|
||||
try {
|
||||
const tx = await ctr.consume(Number(b.memberId), 0, BigInt(b.amount), refToBytes32(b.ref));
|
||||
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; }
|
||||
else { state.lastError = 'consume reverted for burn ' + b.id; break; }
|
||||
} catch (e) {
|
||||
state.lastError = 'burn ' + b.id + ': ' + String(e.shortMessage || e.message).slice(0, 160);
|
||||
// an "Insufficient credits" revert means the member's on-chain balance is
|
||||
// already lower than the engine thinks; leave it pending for the admin to review
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally { running = false; }
|
||||
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; }); }
|
||||
module.exports = { init, tick, status };
|
||||
Reference in New Issue
Block a user