// Firehose: every pump.fun and PumpSwap trade as it lands. import { h, age, usd, sol as fsol, short, store, count } from './util.js'; import { icon } from './icons.js'; import { live, net } from './net.js'; import { avatar } from './parts.js'; import { trackers } from './trackers.js'; const KEEP = 160; export function liveView(root) { const pref = { pool: 'all', side: 'all', min: '0.01', ...store.get('live', {}) }; // under 0.01 SOL is mostly volume bots const win = []; // [ts, buy, sol] of the last 60 s, for the numbers on top let paused = false, held = [], rate = 0; const page = h('div.page.page-in'); const wrap = h('div.wrap.live-wrap'); page.append(wrap); root.append(page); const tile = (k) => { const v = h('span.v', null, '–'), s = h('span.s'); return { el: h('div.tile', null, h('span.k', null, k), v, s), v, s }; }; const tRate = tile('Trades per second'), tBuy = tile('Buys, last minute'), tVol = tile('Volume, last minute'), tBig = tile('Biggest trade, last minute'); const seg = (key, opts) => { const box = h('div.seg'); for (const [v, label] of opts) box.append(h('button' + (pref[key] === v ? '.on' : ''), { onclick: e => { pref[key] = v; save(); for (const b of box.children) b.classList.toggle('on', b === e.currentTarget); } }, label)); return box; }; const minIn = h('input', { value: pref.min, placeholder: '0', inputMode: 'decimal', oninput: e => { pref.min = e.target.value.trim(); save(); } }); const pauseBtn = h('button.btn.sm', { onclick: () => setPause(!paused) }); const state = h('span.live-state'); const tbody = h('tbody'); const table = h('table.t.tape', null, h('thead', null, h('tr', null, h('th', null, 'Time'), h('th', null, 'Coin'), h('th', null, 'Type'), h('th.r', null, 'SOL'), h('th.r', null, 'USD'), h('th.r', null, 'MC'), h('th', null, 'Trader'), h('th', null, 'Where'), h('th'))), tbody); const body = h('div.panel.tape-panel', null, h('div.panel-h', null, h('div.tape-tools', null, seg('pool', [['all', 'All'], ['pump', 'Curve'], ['pump-amm', 'PumpSwap']]), seg('side', [['all', 'Buys + sells'], ['buy', 'Buys'], ['sell', 'Sells']]), h('label.min-sol', { title: 'Hide trades smaller than this' }, 'Min', minIn, h('span', null, 'SOL'))), h('div.tape-tools', null, state, pauseBtn)), h('div.tape-scroll', null, table)); wrap.append( h('div.ph-h', null, h('div', null, h('h1', null, 'Firehose'), h('p', null, 'Every pump.fun and PumpSwap trade, the moment it lands.'))), h('div.tiles', null, tRate.el, tBuy.el, tVol.el, tBig.el), body); table.addEventListener('mouseenter', () => setPause(true, true)); table.addEventListener('mouseleave', () => { if (paused === 'hover') setPause(false); }); function setPause(on, hover) { paused = on ? (hover ? 'hover' : true) : false; pauseBtn.innerHTML = paused ? icon('bolt', 13) + 'Resume' : icon('pause', 13) + 'Pause'; state.textContent = paused ? (held.length ? `Paused · ${count(held.length)} new` : 'Paused') : ''; state.classList.toggle('on', !!paused); if (!paused && held.length) { add(held.slice(-KEEP)); held = []; } } setPause(false); function save() { store.set('live', pref); } function pass(x) { if (pref.pool !== 'all' && x[8] !== pref.pool) return false; if (pref.side === 'buy' && !x[4]) return false; if (pref.side === 'sell' && x[4]) return false; const m = parseFloat(pref.min); if (m > 0 && x[5] < m) return false; return true; } function row(x) { const [ts, mint, sym, img, buy, s, mc, w, pool, sig, name] = x; const solUsd = live.sol || 0; const tk = trackers.name(w); return h('tr.fresh' + (s >= 5 ? '.big' : ''), null, h('td.mut', { 'data-t': ts }, age(ts)), h('td', null, h('a.coin-cell', { href: `/coin/${mint}`, 'data-link': '' }, avatar({ m: mint, s: sym, img }, 26), h('b', null, sym || short(mint)), h('span.mut.tape-name', null, name || ''))), h('td', null, h('b.' + (buy ? 'up' : 'down'), null, buy ? 'Buy' : 'Sell')), h('td.r.' + (buy ? 'up' : 'down'), null, fsol(s)), h('td.r', null, usd(s * solUsd)), h('td.r', null, usd(mc * solUsd)), h('td', null, h('a.w', { href: `/wallet/${w}`, 'data-link': '' }, short(w, 4)), tk ? h('span.tag.tracked', { style: { marginLeft: '6px' } }, tk.name) : ''), h('td', null, h('span.tag' + (pool === 'pump' ? '' : '.mig'), { style: pool === 'pump' ? { color: 'var(--acc)', background: 'var(--acc-bg)' } : null }, pool === 'pump' ? 'Curve' : 'PumpSwap')), h('td.r', null, sig ? h('a.ext', { href: `https://solscan.io/tx/${sig}`, target: '_blank', rel: 'noopener noreferrer', html: icon('ext', 13) }) : '')); } function add(list) { const frag = document.createDocumentFragment(); for (let i = list.length - 1; i >= 0; i--) frag.append(row(list[i])); tbody.prepend(frag); while (tbody.children.length > KEEP) tbody.lastChild.remove(); } const off = live.on('tape', m => { const t = Date.now(); for (const x of m.list) win.push([t, x[4], x[5]]); const shown = m.list.filter(pass).slice(-KEEP); if (!shown.length) return; if (paused) { held.push(...shown); if (held.length > 2000) held = held.slice(-1000); state.textContent = `Paused · ${count(held.length)} new`; return; } add(shown); }); const tick = setInterval(() => { const t = Date.now(); while (win.length && t - win[0][0] > 60000) win.shift(); const last10 = win.filter(x => t - x[0] <= 10000).length; rate = last10 / 10; const buys = win.filter(x => x[1]).length, vol = win.reduce((a, x) => a + x[2], 0), big = win.reduce((a, x) => Math.max(a, x[2]), 0); const solUsd = live.sol || 0; tRate.v.textContent = win.length ? rate.toFixed(rate < 10 ? 1 : 0) : '–'; tRate.s.textContent = win.length ? `${count(win.length)} in the last minute` : 'Waiting for the stream'; tBuy.v.textContent = win.length ? Math.round(buys / win.length * 100) + '%' : '–'; tBuy.s.textContent = win.length ? `${count(buys)} buys · ${count(win.length - buys)} sells` : ''; tVol.v.textContent = win.length ? fsol(vol, vol >= 100 ? 0 : 1) + ' SOL' : '–'; tVol.s.textContent = win.length && solUsd ? usd(vol * solUsd) : ''; tBig.v.textContent = win.length ? fsol(big) + ' SOL' : '–'; tBig.s.textContent = win.length && solUsd ? usd(big * solUsd) : ''; if (!paused) for (const td of tbody.querySelectorAll('td[data-t]')) td.textContent = age(+td.dataset.t, t); }, 1000); net.tape(true); return () => { net.tape(false); off(); clearInterval(tick); }; }