const { useState, useEffect, useRef, useMemo, useCallback } = React;
/* Snipe.Run — views part 4: Market Detail (Polymarket-style + comments), Activity */
/* ============ MARKET DETAIL ============ */
function MarketDetail({ ctx }) {
const { marketId, onBackMarket, authed, onRequireConnect, onOpenTrader } = ctx;
const real = sniIsReal();
const [tf, setTf] = useState('1W');
// Real: marketId is the market slug (the markets list sets id===slug). Hook
// no-ops to null/loading=false when not on the platform so the demo branch runs.
const { data: rm, loading: rmLoading } = useMarket(real ? marketId : null);
// Demo market — also the fallback while a real market is still loading.
const demoM = MARKETS.find((x) => x.id === marketId) || MARKETS[0];
// Map the real market detail into the shape the view renders. YES price comes
// from the live book of the first clob token (Polymarket: tokens[0] = YES).
const m = useMemo(() => {
if (!real || !rm) return demoM;
const yesTok = (rm.tokens && rm.tokens[0]) || null;
const px = (rm.prices && yesTok && rm.prices[yesTok]) || null;
// mid of bid/ask, fall back to bid, then 50%.
let yes = 50;
if (px) {
const bid = +px.bid || 0, ask = +px.ask || 0;
const mid = (bid && ask) ? (bid + ask) / 2 : (bid || ask);
if (mid) yes = Math.round(mid * 100);
}
yes = Math.max(1, Math.min(99, yes));
return {
id: rm.slug || marketId,
slug: rm.slug || marketId,
q: rm.q || 'Polymarket market',
cat: rm.cat || 'Markets',
desc: rm.desc || null,
ico: '◈',
yes,
chg: 0,
vol: +rm.vol || 0,
liq: +rm.liq || 0,
close: rm.end ? new Date(rm.end).toLocaleDateString('en-US', { month: 'short', day: '2-digit' }) : '—',
end: rm.end || null,
tokens: rm.tokens || [],
prices: rm.prices || {},
condition_id: rm.condition_id || null,
};
}, [real, rm, demoM, marketId]);
const series = useMemo(() => {
const r = seedRand('mk' + (m.id || marketId) + tf); let v = m.yes; const out = [];
for (let i = 0; i < 60; i++) { v += (r() - 0.5) * 6; v = Math.max(8, Math.min(92, v)); out.push(v); }
out[out.length - 1] = m.yes; return out;
}, [m.id, m.yes, tf, marketId]);
// Top holders / "copy traders here" have no real source yet — demo only.
const holders = useMemo(() => {
if (real) return [];
const r = seedRand('hold' + m.id);
return TRADERS.slice().sort(() => r() - 0.5).slice(0, 6).map((t, i) => ({ name: t.name, id: t.id, side: r() > 0.3 ? 'YES' : 'NO', shares: Math.floor(r() * 80000 + 8000), avg: Math.floor(r() * 30 + 40) }));
}, [m.id, real]);
const inMarketTraders = holders.filter((h) => h.side === 'YES').slice(0, 3);
// Loading shell for the first real fetch (keeps hook order stable above).
if (real && rmLoading && !rm) {
return (
);
}
if (real && !rm) {
return (
Back to markets
Market unavailable. Back to markets
);
}
return (
Back to markets
{/* header */}
{m.ico}
{m.cat}
Live
{m.q}
{moneyShort(m.vol)} Vol {moneyShort(real ? (m.liq || 0) : m.vol * 0.4)} Liquidity Closes {m.close}{real ? '' : ', 2026'} {!real && {(holders.length * 318).toLocaleString()} holders }
Chance (YES)
{m.yes}%
{real
?
Live price
:
= 0 ? 'up' : 'down'}>{m.chg >= 0 ? '▲' : '▼'} {Math.abs(m.chg)}¢ 24h }
{/* LEFT */}
Probability history
{['1H', '1D', '1W', '1M', 'ALL'].map((t) => setTf(t)}>{t} )}
{real &&
Live YES probability {m.yes}% · price history chart coming soon.
}
{/* top holders — demo only; no real holder source yet */}
{real ? (
On-chain holder breakdown coming soon.
) : (
Trader Position Shares Avg
{holders.map((h, i) => (
onOpenTrader(h.id)}>
{h.side}
{h.shares.toLocaleString()}
{h.avg}¢
e.stopPropagation()}> authed ? onOpenTrader(h.id) : onRequireConnect()}>Copy
))}
)}
{/* comments — demo only; real comment feed not available yet */}
{real ? (
Market comments are coming soon.
) : (
)}
{/* RIGHT */}
{real ? (
Copy traders here
Per-market copyable traders coming soon. Browse the leaderboard to copy a trader across all their positions.
) : (
Copy traders here
{inMarketTraders.length}
{inMarketTraders.map((h) => {
const t = TRADERS.find((x) => x.id === h.id);
return (
onOpenTrader(h.id)}>
{h.name}
{pct(t.roi)} · {t.win}% win
authed ? onOpenTrader(h.id) : onRequireConnect()}>Copy
);
})}
)}
Resolution
{real && m.desc
? m.desc
: <>This market resolves YES if the outcome is officially confirmed by the listed source on or before {m.close}{real ? '' : ', 2026'}. Resolves NO otherwise.>}
Resolver UMA Oracle ↗
);
}
function OrderPanel({ market, real, authed, onRequireConnect }) {
const [side, setSide] = useState('YES');
const [amt, setAmt] = useState(100);
const [done, setDone] = useState(false);
const [busy, setBusy] = useState(false);
const [err, setErr] = useState(null);
const price = side === 'YES' ? market.yes : 100 - market.yes;
const shares = (amt / (price / 100)).toFixed(0);
// Real token ids — Polymarket clobTokenIds: [0]=YES, [1]=NO.
const tokens = (real && market.tokens) || [];
const tokenId = side === 'YES' ? (tokens[0] || null) : (tokens[1] || null);
const buy = () => {
if (!authed) return onRequireConnect();
setErr(null);
if (real) {
if (!tokenId) { setErr('Market not tradable right now.'); return; }
if (!amt || amt <= 0) { setErr('Enter an amount.'); return; }
setBusy(true);
sniApi('market/order', { method: 'POST', body: JSON.stringify({ token_id: tokenId, side: 'BUY', usdc: amt }) })
.then((r) => {
setBusy(false);
if (r && r.ok) { setDone(true); setTimeout(() => setDone(false), 2600); }
else setErr((r && (r.message || r.code)) || 'Order failed.');
})
.catch(() => { setBusy(false); setErr('Order failed.'); });
return;
}
// demo: instant optimistic fill
setDone(true); setTimeout(() => setDone(false), 2200);
};
return (
{ setSide('YES'); setErr(null); }}>Buy Yes · {market.yes}¢
{ setSide('NO'); setErr(null); }}>Buy No · {100 - market.yes}¢
{err &&
{err}
}
{done
?
{real ? 'Paper order placed' : 'Filled in 7ms'}
:
{busy ? 'Placing…' : (authed ? `Buy ${side} · ${money(amt, 0)}` : 'Connect to trade')}}
{real ? 'Paper order — simulated against the live book' : 'Or auto-mirror this via a copied trader'}
);
}
function CommentsSection({ market, authed, onRequireConnect }) {
const seeded = useMemo(() => {
const r = seedRand('cm' + market.id);
const base = COMMENTS.slice().sort(() => r() - 0.5).slice(0, 5);
return base.map((c, i) => ({ ...c, id: 'c' + i }));
}, [market.id]);
const [list, setList] = useState(seeded);
const [text, setText] = useState('');
const [sort, setSort] = useState('top');
useEffect(() => { setList(seeded); }, [seeded]);
const post = () => {
if (!authed) return onRequireConnect();
if (!text.trim()) return;
setList((l) => [{ id: 'me' + Date.now(), name: 'degen_whale', side: 'YES', text: text.trim(), likes: 0, time: 'now', badge: 'You', mine: true }, ...l]);
setText('');
};
const shown = sort === 'top' ? [...list].sort((a, b) => b.likes - a.likes) : list;
return (
Comments {list.length}
setSort('top')}>Top setSort('new')}>Newest
{shown.map((c) => )}
);
}
function Comment({ c }) {
const [likes, setLikes] = useState(c.likes);
const [liked, setLiked] = useState(false);
return (
{c.name}
{c.side} holder
{c.badge && {c.badge} }
{c.time}
{c.text}
{ setLiked(!liked); setLikes((n) => n + (liked ? -1 : 1)); }} style={{ background: 'none', border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6, color: liked ? 'var(--green)' : 'var(--tx-3)', fontFamily: 'var(--ui)', fontWeight: 600, fontSize: 13.5 }}>
{likes}
Reply
);
}
/* ============ ACTIVITY ============ */
function Activity({ ctx }) {
const live = useLiveData();
const real = sniIsReal();
const [filter, setFilter] = useState('all');
const [rows, setRows] = useState(() => ACTIVITY.map((a, i) => ({ ...a, id: i })));
const filters = [['all', 'All'], ['fill', 'Copies'], ['tp', 'Profits'], ['deposit', 'Wallet'], ['rank', 'System']];
useInterval(() => {
if (real) return;
const tr = TRADERS[Math.floor(Math.random() * TRADERS.length)];
const mk = MARKETS[Math.floor(Math.random() * MARKETS.length)];
setRows((r) => [{ id: Date.now(), type: 'fill', side: Math.random() > 0.4 ? 'BUY' : 'SELL', trader: tr.name, market: mk.q, amt: Math.floor(Math.random() * 12000 + 500), price: mk.yes, ms: Math.floor(Math.random() * 14 + 7), t: 'just now', fresh: true }, ...r].slice(0, 40));
}, 2600);
// Real: live fills + closes from the worker (no demo fallback). Demo otherwise.
const baseRows = real ? sniMapActivity(live) : rows;
const stats = sniStats(live);
const realizedReal = +stats.realized_pnl || 0;
const tradesReal = +stats.trades || 0;
const shown = filter === 'all' ? baseRows : baseRows.filter((r) => filter === 'tp' ? (r.type === 'tp' || r.type === 'sl') : filter === 'rank' ? (r.type === 'rank' || r.type === 'resolve') : r.type === filter);
return (
= 0 ? '+' : '') + moneyShort(realizedReal)) : '+$18.4k'} icon="award" accent={realizedReal >= 0} />
} icon="gauge" accent />
Activity
Every copy, fill and payout across your wallets · for raw engine telemetry see Bot Logs
{filters.map(([id, l]) => setFilter(id)}>{l} )}
{shown.map((a) =>
)}
{!shown.length &&
{real ? 'No activity yet — start the bot to copy live fills.' : 'No activity in this category yet.'}
}
);
}
function ActivityRow({ a, onOpenTrader }) {
const cfg = {
fill: { ico: a.side === 'BUY' ? 'arrowUp' : 'arrowDown', col: a.side === 'BUY' ? 'var(--green)' : 'var(--red)', bg: a.side === 'BUY' ? 'rgba(22,255,122,0.1)' : 'rgba(255,77,94,0.1)' },
tp: { ico: 'award', col: 'var(--green)', bg: 'rgba(22,255,122,0.1)' },
sl: { ico: 'arrowDown', col: 'var(--red)', bg: 'rgba(255,77,94,0.1)' },
rank: { ico: 'trophy', col: 'var(--amber)', bg: 'rgba(255,194,75,0.1)' },
deposit: { ico: 'arrowDownC', col: 'var(--cyan)', bg: 'rgba(63,224,255,0.1)' },
resolve: { ico: 'flag', col: 'var(--violet)', bg: 'rgba(154,140,255,0.1)' },
}[a.type] || { ico: 'pulse', col: 'var(--tx-2)', bg: 'var(--bg-2)' };
return (
{a.type === 'fill' && <>Copied onOpenTrader && onOpenTrader()}>{a.trader} · {a.side} {a.market} >}
{a.type === 'tp' && <>Take-profit hit on {a.trader} 's {a.market} >}
{a.type === 'sl' && <>Stop-loss triggered on {a.trader} 's {a.market} >}
{a.type === 'rank' && <>{a.trader} {a.detail}>}
{a.type === 'deposit' && <>{a.detail}>}
{a.type === 'resolve' && <>Market {a.market} {a.detail}>}
{a.type === 'fill' && <>{moneyShort(a.amt)} @ {a.price}¢{a.ms ? <> · filled in {a.ms}ms > : null}>}
{(a.type === 'tp' || a.type === 'sl') && = 0 ? 'var(--green)' : 'var(--red)' }}>{a.pnl >= 0 ? '+' : ''}{money(a.pnl)} realized }
{a.t}
);
}
Object.assign(window, { MarketDetail, Activity });