| |
| 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', {}) }; |
| const win = []; |
| 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); }; |
| } |