fix: finalize-round.sh set -e silent exit on already-finalized round#3
fix: finalize-round.sh set -e silent exit on already-finalized round#3ClawdiaETH wants to merge 5 commits intofeat/twitter-cap-exceeded-guardfrom
Conversation
- base.drpc.org free tier rejects ranges >10k blocks - mainnet.base.org also limits to 10k AND was returning 503 - paginate the 25k-block SeedAndRulerRevealed lookup in 9500-block chunks - switch RPC to base-rpc.publicnode.com (reliable, no range issues)
getCurrentRound() returns the new round N, but SeedAndRulerRevealed event is for round N-1 (the round just revealed). Event filter was using roundId=N and finding nothing, causing silent failure every round. Fix: derive revealedRoundId = roundId - 1, use for event filter and announcement text.
cast send returns non-zero when round is already finalized, which caused set -e to kill the script before the 'Already finalized' check ran. Adding || true prevents the premature exit while preserving all logic. Observed: round 7 was already finalized but script reported exit 1 (2026-02-27).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Hardens Written by Cursor Bugbot for commit 4fe9ebd. This will update automatically on new commits. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Wrong round ID used for reveal announcement
- Updated the social reveal script to use the current open round ID for event lookup and announcement text instead of subtracting one.
Preview (4fe9ebd8b4)
diff --git a/bot/spellblock-finalize.mjs b/bot/spellblock-finalize.mjs
--- a/bot/spellblock-finalize.mjs
+++ b/bot/spellblock-finalize.mjs
@@ -19,7 +19,7 @@
const __dir = dirname(fileURLToPath(import.meta.url));
const CONTRACT = '0x43F8658F3E85D1F92289e3036168682A9D14c683';
-const RPC = 'https://base.drpc.org';
+const RPC = 'https://1rpc.io/base'; // paginated getLogs in 9500-block chunks — most public RPCs limit to 10k
const FOUNDRY = '/Users/starl3xx/.foundry/bin';
const BOT_BANKR_HANDLE = 'ClawdiaBotAI';
const CLAWDIA_DECIMALS = 18n;
@@ -99,12 +99,17 @@
const client = createPublicClient({ chain: base, transport: http(RPC) });
const latest = await client.getBlockNumber();
- const seedLogs = await client.getLogs({
- address: CONTRACT,
- event: parseAbiItem('event SeedAndRulerRevealed(uint256 indexed roundId, uint8 spellId, bytes32 spellParam, uint8[3] validLengths)'),
- fromBlock: latest - 25000n, // ~14h of Base blocks (2s/block) covers reveal→finalize gap
- toBlock: 'latest',
- });
+ // Paginate getLogs in 9500-block chunks — public RPCs limit ranges to 10k blocks
+ const CHUNK = 9500n;
+ const lookback = 25000n;
+ const startBlock = latest - lookback;
+ const seedAbi = parseAbiItem('event SeedAndRulerRevealed(uint256 indexed roundId, uint8 spellId, bytes32 spellParam, uint8[3] validLengths)');
+ const seedLogs = [];
+ for (let from = startBlock; from <= latest; from += CHUNK) {
+ const to = from + CHUNK - 1n < latest ? from + CHUNK - 1n : latest;
+ const chunk = await client.getLogs({ address: CONTRACT, event: seedAbi, fromBlock: from, toBlock: to });
+ seedLogs.push(...chunk);
+ }
const seedLog = seedLogs.filter(l => Number(l.args.roundId) === round.round_id).pop();
diff --git a/bot/spellblock-social-reveal.mjs b/bot/spellblock-social-reveal.mjs
--- a/bot/spellblock-social-reveal.mjs
+++ b/bot/spellblock-social-reveal.mjs
@@ -16,7 +16,7 @@
const __dir = dirname(fileURLToPath(import.meta.url));
const CONTRACT = '0x43F8658F3E85D1F92289e3036168682A9D14c683';
-const RPC = 'https://base.drpc.org';
+const RPC = 'https://1rpc.io/base';
const SPELL_NAMES = {
0: { name: 'Veto 🚫', desc: (p) => `word must NOT contain ${p}` },
@@ -79,27 +79,38 @@
const { base } = await import('viem/chains');
const client = createPublicClient({ chain: base, transport: http(RPC) });
- // Get current round from DB
+ // Get current round from DB — reveal-seed-and-ruler.sh reveals for this same
+ // currently open round.
const round = await db.getCurrentRound();
if (!round) { log('No open round in DB'); await db.end(); return; }
const roundId = round.round_id;
- log(`Round ${roundId} | letters: ${round.letters}`);
+ const revealedRoundId = roundId; // The round that was just revealed on-chain
+ log(`Current round: ${roundId} | letters: ${round.letters} | Announcing reveal for round: ${revealedRoundId}`);
// Read SeedAndRulerRevealed event from contract
+ // Paginate in 9500-block chunks to stay under public RPC limits (10k max)
const latest = await client.getBlockNumber();
- const logs = await client.getLogs({
- address: CONTRACT,
- event: parseAbiItem(
- 'event SeedAndRulerRevealed(uint256 indexed roundId, uint8 spellId, bytes32 spellParam, uint8[3] validLengths)'
- ),
- fromBlock: latest - 25000n, // ~14h of Base blocks; covers the reveal→post window
- toBlock: 'latest',
- });
+ const LOOKBACK = 25000n;
+ const CHUNK = 9500n;
+ const eventAbi = parseAbiItem(
+ 'event SeedAndRulerRevealed(uint256 indexed roundId, uint8 spellId, bytes32 spellParam, uint8[3] validLengths)'
+ );
+ const logs = [];
+ for (let from = latest - LOOKBACK; from <= latest; from += CHUNK) {
+ const to = from + CHUNK - 1n < latest ? from + CHUNK - 1n : latest;
+ const chunk = await client.getLogs({
+ address: CONTRACT,
+ event: eventAbi,
+ fromBlock: from,
+ toBlock: to,
+ });
+ logs.push(...chunk);
+ }
- const revealLog = logs.filter(l => Number(l.args.roundId) === roundId).pop();
+ const revealLog = logs.filter(l => Number(l.args.roundId) === revealedRoundId).pop();
if (!revealLog) {
- log('❌ No SeedAndRulerRevealed event found — round not yet revealed or event window too narrow');
+ log(`❌ No SeedAndRulerRevealed event found for round ${revealedRoundId} — not yet revealed or event window too narrow`);
await db.end();
return;
}
@@ -118,7 +129,7 @@
// Compose announcement
const text =
- `🔮 SpellBlock Round ${roundId} — Spell + Ruler revealed!\n\n` +
+ `🔮 SpellBlock Round ${revealedRoundId} — Spell + Ruler revealed!\n\n` +
`${spell.name}: ${spellDesc}\n` +
`📏 Valid lengths: ${validLengths.join(', ')}\n\n` +
`Scoring + payouts happen automatically at 15:45 UTC (9:45 AM CT).\n\n` +
diff --git a/scripts/finalize-round.sh b/scripts/finalize-round.sh
--- a/scripts/finalize-round.sh
+++ b/scripts/finalize-round.sh
@@ -28,11 +28,13 @@
fi
echo "Finalizing Round $CURRENT_ROUND..."
+# Use || true so set -e doesn't exit on non-zero cast return (e.g. "Already finalized"),
+# letting the if/elif/else below handle all cases gracefully.
TX=$(/Users/starl3xx/.foundry/bin/cast send $CONTRACT \
"finalizeRound()" \
--private-key $PRIVATE_KEY \
--rpc-url https://mainnet.base.org \
- --json 2>&1)
+ --json 2>&1) || true
if echo "$TX" | grep -q "transactionHash"; then
TX_HASH=$(echo $TX | /opt/homebrew/bin/jq -r '.transactionHash')
Problem
set -ecauses the script to exit with code 1 whencast send finalizeRound()returns non-zero (e.g. round is already finalized), before theelif grep "Already finalized"check runs. This produces noisy false failures with no actionable info.Observed: Round 7 was already finalized but the script reported
exit 1on 2026-02-27 09:48 AM.Fix
Wrap
cast sendwith|| trueso the non-zero exit code doesn't triggerset -e. All outcome handling (success, already-finalized, real error) remains in theif/elif/elseblock below.Why
|| trueis safe hereThe output is captured in
$TXregardless of exit code. The downstreamif echo "$TX" | grep -qchecks handle all cases correctly — this was always the intent, just blocked byset -e.