Skip to content

Commit 844e535

Browse files
ScavieFaeclaude
andcommitted
Live bet activity in tournament viewer: show onchain BetPlaced events
- Added BetPlaced event to PredictionPool ABI - New /pools/{id}/bets endpoint reads BetPlaced events from chain - Activity feed shows bets in orange (addr + amount + side) alongside match results in green. Polls every 3s with bracket data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b76d95d commit 844e535

4 files changed

Lines changed: 204 additions & 34 deletions

File tree

arena/server.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1693,6 +1693,57 @@ def get_pool_odds(pool_id: int) -> dict[str, Any]:
16931693
raise HTTPException(status_code=500, detail=str(e))
16941694

16951695

1696+
@app.get("/pools/{pool_id}/bets")
1697+
def get_pool_bets(pool_id: int) -> dict[str, Any]:
1698+
"""Read recent BetPlaced events for a pool from the PredictionPool contract."""
1699+
pool_address = os.environ.get("PREDICTION_POOL")
1700+
if not pool_address:
1701+
raise HTTPException(status_code=503, detail="PREDICTION_POOL not configured")
1702+
1703+
rpc_url = os.environ.get("MONAD_RPC_URL", "https://rpc.monad.xyz")
1704+
1705+
try:
1706+
from nojohns.contract import get_prediction_pool_contract
1707+
1708+
w3, contract = get_prediction_pool_contract(rpc_url, pool_address)
1709+
latest = w3.eth.block_number
1710+
# Scan last 2000 blocks (~13 min on Monad at 0.4s blocks)
1711+
from_block = max(0, latest - 2000)
1712+
1713+
# Monad caps getLogs to 100 blocks per call — scan in chunks
1714+
all_events = []
1715+
chunk_size = 100
1716+
block = from_block
1717+
while block <= latest:
1718+
end = min(block + chunk_size - 1, latest)
1719+
try:
1720+
events = contract.events.BetPlaced().get_logs(
1721+
fromBlock=block,
1722+
toBlock=end,
1723+
argument_filters={"poolId": pool_id},
1724+
)
1725+
all_events.extend(events)
1726+
except Exception:
1727+
pass # Skip failed chunks
1728+
block = end + 1
1729+
1730+
bets = []
1731+
for e in all_events:
1732+
bets.append({
1733+
"bettor": e.args.bettor,
1734+
"betOnA": e.args.betOnA,
1735+
"amount": str(e.args.amount),
1736+
"amount_mon": round(e.args.amount / 10**18, 4),
1737+
"block": e.blockNumber,
1738+
})
1739+
1740+
return {"pool_id": pool_id, "bets": bets}
1741+
except ImportError:
1742+
raise HTTPException(status_code=503, detail="web3 not installed")
1743+
except Exception as e:
1744+
raise HTTPException(status_code=500, detail=str(e))
1745+
1746+
16961747
# ======================================================================
16971748
# Faucet Endpoint
16981749
# ======================================================================

nojohns/contract.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,17 @@ def is_recorded(
347347
"outputs": [{"name": "", "type": "uint256"}],
348348
"stateMutability": "view",
349349
},
350+
# --- Events ---
351+
{
352+
"type": "event",
353+
"name": "BetPlaced",
354+
"inputs": [
355+
{"name": "poolId", "type": "uint256", "indexed": True},
356+
{"name": "bettor", "type": "address", "indexed": True},
357+
{"name": "betOnA", "type": "bool", "indexed": False},
358+
{"name": "amount", "type": "uint256", "indexed": False},
359+
],
360+
},
350361
]
351362

352363

tournaments/viewer.html

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,38 +1229,92 @@
12291229

12301230
// ── Activity Feed ────────────────────────────────────────────
12311231

1232-
function updateActivity(data) {
1232+
// ── Activity feed: bets + match results ────────────────────
1233+
1234+
let activityItems = []; // { text, type, time }
1235+
let lastBetCount = {}; // pool_id -> count of bets we've seen
1236+
1237+
function addActivity(text, type) {
1238+
activityItems.unshift({ text, type, time: Date.now() });
1239+
if (activityItems.length > 10) activityItems.length = 10;
1240+
renderActivity();
1241+
}
1242+
1243+
function renderActivity() {
12331244
const list = document.getElementById('activity-list');
1234-
const rounds = data.bracket?.rounds || [];
1235-
const results = [];
1245+
if (activityItems.length === 0) {
1246+
list.innerHTML = '<li>Waiting for bets...</li>';
1247+
return;
1248+
}
1249+
list.innerHTML = activityItems.map(a => {
1250+
const cls = a.type === 'bet' ? 'style="color:#ff6b35;"'
1251+
: a.type === 'win' ? 'style="color:#00ff88;"'
1252+
: '';
1253+
return `<li ${cls}>${a.text}</li>`;
1254+
}).join('');
1255+
}
12361256

1257+
function updateActivity(data) {
1258+
// Add match results
1259+
const rounds = data.bracket?.rounds || [];
12371260
for (const r of rounds) {
12381261
for (const m of r) {
12391262
if ((m.status === 'complete' || m.status === 'coinflip') && m.winner) {
1263+
const key = `result-${m.round}-${m.slot}`;
1264+
if (activityItems.some(a => a._key === key)) continue;
12401265
const loser = (m.entry_a && m.winner.name === m.entry_a.name) ? m.entry_b : m.entry_a;
12411266
const label = m.status === 'coinflip' ? 'COIN' : 'WIN';
12421267
const scoreStr = (m.score_a != null && m.score_b != null)
1243-
? ` (${m.score_a}-${m.score_b})`
1244-
: '';
1245-
results.push({
1268+
? ` (${m.score_a}-${m.score_b})` : '';
1269+
const item = {
12461270
text: `${escHtml(m.winner.name)} ${label}${scoreStr} vs ${loser ? escHtml(loser.name) : 'BYE'}`,
1247-
round: m.round,
1248-
slot: m.slot,
1249-
});
1271+
type: 'win', time: Date.now(), _key: key,
1272+
};
1273+
activityItems.unshift(item);
12501274
}
12511275
}
12521276
}
1277+
if (activityItems.length > 10) activityItems.length = 10;
1278+
renderActivity();
12531279

1254-
if (results.length === 0) {
1255-
list.innerHTML = '<li>No results yet</li>';
1256-
return;
1280+
// Poll for bet activity on current pool
1281+
pollBetActivity(data);
1282+
}
1283+
1284+
async function pollBetActivity(data) {
1285+
// Find current pool_id from playing match
1286+
const rounds = data.bracket?.rounds || [];
1287+
let poolId = null;
1288+
let entryA = null, entryB = null;
1289+
for (const r of rounds) {
1290+
for (const m of r) {
1291+
if (m.pool_id && (m.status === 'playing' || m.status === 'complete')) {
1292+
poolId = m.pool_id;
1293+
entryA = m.entry_a;
1294+
entryB = m.entry_b;
1295+
}
1296+
}
12571297
}
1298+
if (!poolId) return;
12581299

1259-
// Show most recent first, limit to 6
1260-
results.reverse();
1261-
list.innerHTML = results.slice(0, 6).map(r =>
1262-
`<li>${r.text}</li>`
1263-
).join('');
1300+
try {
1301+
const resp = await fetch(`${arenaBase}/pools/${poolId}/bets`);
1302+
if (!resp.ok) return;
1303+
const result = await resp.json();
1304+
const bets = result.bets || [];
1305+
const prev = lastBetCount[poolId] || 0;
1306+
1307+
if (bets.length > prev) {
1308+
// New bets since last poll
1309+
const newBets = bets.slice(prev);
1310+
for (const b of newBets) {
1311+
const side = b.betOnA ? (entryA?.name || 'Player A') : (entryB?.name || 'Player B');
1312+
const addr = b.bettor.slice(0, 6) + '...' + b.bettor.slice(-4);
1313+
addActivity(`${addr} bet ${b.amount_mon} MON on ${escHtml(side)}`, 'bet');
1314+
}
1315+
}
1316+
lastBetCount[poolId] = bets.length;
1317+
} catch (e) { /* silent */ }
12641318
}
12651319

12661320
// ── Live Match WebSocket Stream ───────────────────────────────

web/public/tournament/index.html

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,38 +1229,92 @@
12291229

12301230
// ── Activity Feed ────────────────────────────────────────────
12311231

1232-
function updateActivity(data) {
1232+
// ── Activity feed: bets + match results ────────────────────
1233+
1234+
let activityItems = []; // { text, type, time }
1235+
let lastBetCount = {}; // pool_id -> count of bets we've seen
1236+
1237+
function addActivity(text, type) {
1238+
activityItems.unshift({ text, type, time: Date.now() });
1239+
if (activityItems.length > 10) activityItems.length = 10;
1240+
renderActivity();
1241+
}
1242+
1243+
function renderActivity() {
12331244
const list = document.getElementById('activity-list');
1234-
const rounds = data.bracket?.rounds || [];
1235-
const results = [];
1245+
if (activityItems.length === 0) {
1246+
list.innerHTML = '<li>Waiting for bets...</li>';
1247+
return;
1248+
}
1249+
list.innerHTML = activityItems.map(a => {
1250+
const cls = a.type === 'bet' ? 'style="color:#ff6b35;"'
1251+
: a.type === 'win' ? 'style="color:#00ff88;"'
1252+
: '';
1253+
return `<li ${cls}>${a.text}</li>`;
1254+
}).join('');
1255+
}
12361256

1257+
function updateActivity(data) {
1258+
// Add match results
1259+
const rounds = data.bracket?.rounds || [];
12371260
for (const r of rounds) {
12381261
for (const m of r) {
12391262
if ((m.status === 'complete' || m.status === 'coinflip') && m.winner) {
1263+
const key = `result-${m.round}-${m.slot}`;
1264+
if (activityItems.some(a => a._key === key)) continue;
12401265
const loser = (m.entry_a && m.winner.name === m.entry_a.name) ? m.entry_b : m.entry_a;
12411266
const label = m.status === 'coinflip' ? 'COIN' : 'WIN';
12421267
const scoreStr = (m.score_a != null && m.score_b != null)
1243-
? ` (${m.score_a}-${m.score_b})`
1244-
: '';
1245-
results.push({
1268+
? ` (${m.score_a}-${m.score_b})` : '';
1269+
const item = {
12461270
text: `${escHtml(m.winner.name)} ${label}${scoreStr} vs ${loser ? escHtml(loser.name) : 'BYE'}`,
1247-
round: m.round,
1248-
slot: m.slot,
1249-
});
1271+
type: 'win', time: Date.now(), _key: key,
1272+
};
1273+
activityItems.unshift(item);
12501274
}
12511275
}
12521276
}
1277+
if (activityItems.length > 10) activityItems.length = 10;
1278+
renderActivity();
12531279

1254-
if (results.length === 0) {
1255-
list.innerHTML = '<li>No results yet</li>';
1256-
return;
1280+
// Poll for bet activity on current pool
1281+
pollBetActivity(data);
1282+
}
1283+
1284+
async function pollBetActivity(data) {
1285+
// Find current pool_id from playing match
1286+
const rounds = data.bracket?.rounds || [];
1287+
let poolId = null;
1288+
let entryA = null, entryB = null;
1289+
for (const r of rounds) {
1290+
for (const m of r) {
1291+
if (m.pool_id && (m.status === 'playing' || m.status === 'complete')) {
1292+
poolId = m.pool_id;
1293+
entryA = m.entry_a;
1294+
entryB = m.entry_b;
1295+
}
1296+
}
12571297
}
1298+
if (!poolId) return;
12581299

1259-
// Show most recent first, limit to 6
1260-
results.reverse();
1261-
list.innerHTML = results.slice(0, 6).map(r =>
1262-
`<li>${r.text}</li>`
1263-
).join('');
1300+
try {
1301+
const resp = await fetch(`${arenaBase}/pools/${poolId}/bets`);
1302+
if (!resp.ok) return;
1303+
const result = await resp.json();
1304+
const bets = result.bets || [];
1305+
const prev = lastBetCount[poolId] || 0;
1306+
1307+
if (bets.length > prev) {
1308+
// New bets since last poll
1309+
const newBets = bets.slice(prev);
1310+
for (const b of newBets) {
1311+
const side = b.betOnA ? (entryA?.name || 'Player A') : (entryB?.name || 'Player B');
1312+
const addr = b.bettor.slice(0, 6) + '...' + b.bettor.slice(-4);
1313+
addActivity(`${addr} bet ${b.amount_mon} MON on ${escHtml(side)}`, 'bet');
1314+
}
1315+
}
1316+
lastBetCount[poolId] = bets.length;
1317+
} catch (e) { /* silent */ }
12641318
}
12651319

12661320
// ── Live Match WebSocket Stream ───────────────────────────────

0 commit comments

Comments
 (0)