Credits: reserve live campaign budgets so purchased credits can't be over-committed; burner dry-runs and skips reverting burns; coaching flags directs not bound to you on-chain
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ 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 };
|
||||
const state = { enabled: false, address: null, signer: null, balanceWei: '0', lastRun: 0, lastError: null, burned: 0, lastTx: null, mismatch: false, skipped: {} };
|
||||
let running = false;
|
||||
|
||||
function keyHex() {
|
||||
@@ -53,6 +53,10 @@ async function tick() {
|
||||
try {
|
||||
// Polygon nodes reject low priority fees and some public RPCs answer fee
|
||||
// queries with 500s: set the fees ourselves from the server's estimate
|
||||
// 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.ref)); }
|
||||
catch (e) { state.skipped[b.id] = String(e.reason || e.shortMessage || e.message).slice(0, 120); continue; }
|
||||
const g = await chain.suggestedFees();
|
||||
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);
|
||||
@@ -64,8 +68,8 @@ async function tick() {
|
||||
// a node error (500, timeout, rate limit): move to the next RPC for the next tick.
|
||||
// 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
|
||||
if (/server response|timeout|rate|429|503|502|500/i.test(String(e.message))) rotateRpc();
|
||||
break;
|
||||
if (/server response|timeout|rate|429|503|502|500/i.test(String(e.message))) { rotateRpc(); break; }
|
||||
state.skipped[b.id] = String(e.reason || e.shortMessage || e.message).slice(0, 120);
|
||||
}
|
||||
}
|
||||
} finally { running = false; }
|
||||
|
||||
@@ -61,16 +61,21 @@ async function describe(acct, now = Date.now()) {
|
||||
const stalled = rung < 6 && quietDays >= STALL_DAYS;
|
||||
const R = RUNGS[rung];
|
||||
return { rung, label: R.label, next: R.next, say: R.say, quietDays, stalled, buyerCount: mm ? mm.buyerCount : 0,
|
||||
counted: !!(mm && mm.countedAsBuyer), joined: acct.created, lastSeen: acct.lastSeen || 0 };
|
||||
counted: !!(mm && mm.countedAsBuyer), joined: acct.created, lastSeen: acct.lastSeen || 0, onchainSponsorId: mm ? mm.sponsorId : null };
|
||||
}
|
||||
// coaching view for a sponsor: every direct, ladder rung, stalled flag, what to say
|
||||
async function coachView(email) {
|
||||
const levels = await accounts.downline(email, 1);
|
||||
const directs = levels.length ? levels[0].members : [];
|
||||
const me = await accounts.byEmail(email);
|
||||
const myId = me ? (me.memberId || 0) : 0;
|
||||
const now = Date.now();
|
||||
const out = [];
|
||||
for (const d of directs) {
|
||||
const c = await describe(d, now);
|
||||
// bound: the contract pays whoever the member registered under. A direct who activated with
|
||||
// no sponsor (or a different one) is in this line on the site but pays this member nothing.
|
||||
c.bound = c.onchainSponsorId == null ? null : (myId > 0 && c.onchainSponsorId === myId);
|
||||
out.push(Object.assign({ email: d.email, name: d.username ? '@' + d.username : (d.memberId ? 'member #' + d.memberId : d.email.replace(/^(.).*(@.*)$/, '$1***$2')), memberId: d.memberId || 0 }, c));
|
||||
}
|
||||
// most actionable first: stalled lowest rung, then quiet days
|
||||
|
||||
+1
-1
@@ -990,7 +990,7 @@
|
||||
const d = r.directs || [];
|
||||
$('coachSummary').innerHTML = d.length ? '<b>' + d.length + '</b> direct' + (d.length === 1 ? '' : 's') + ' · <b>' + r.stalled + '</b> quiet for 3+ days' + (r.stalled ? ' · start at the top' : '') : '';
|
||||
if (!d.length) { el.innerHTML = '<p class="muted small">No directs yet. When someone joins through your link they show up here with their next step.</p>'; return; }
|
||||
el.innerHTML = d.map(x => '<div class="lin-row' + (x.stalled ? ' own' : '') + '"><span class="nm">' + esc(x.name) + (x.stalled ? ' <span class="badge amber">quiet ' + x.quietDays + 'd</span>' : '') + '</span>'
|
||||
el.innerHTML = d.map(x => '<div class="lin-row' + (x.stalled ? ' own' : '') + '"><span class="nm">' + esc(x.name) + (x.stalled ? ' <span class="badge amber">quiet ' + x.quietDays + 'd</span>' : '') + (x.bound === false ? ' <span class="badge amber" title="On-chain this member registered under ' + (x.onchainSponsorId ? 'member #' + x.onchainSponsorId : 'no sponsor') + ', so the contract pays their purchases there, not to you. Sponsor binding is permanent at first activation.">not bound to you on-chain</span>' : '') + '</span>'
|
||||
+ '<span class="em">' + esc(x.label) + ' → ' + esc(x.next) + '</span>'
|
||||
+ '<span class="id">rung ' + x.rung + '/6</span>'
|
||||
+ '<span class="dt">' + (x.buyerCount ? x.buyerCount + ' buyer' + (x.buyerCount === 1 ? '' : 's') : '') + '</span>'
|
||||
|
||||
+1
-1
@@ -858,7 +858,7 @@
|
||||
<script src="/assets/common.js?v=20260910b"></script>
|
||||
<script src="/assets/wallet.js?v=20260910a"></script>
|
||||
<script src="/assets/promo.js?v=20260909b"></script>
|
||||
<script src="/assets/my.js?v=20260910g"></script>
|
||||
<script src="/assets/my.js?v=20260910h"></script>
|
||||
<script src="/assets/chat.js?v=20260907l"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user