Back to OScope
oscope/public/js/live.js
Code
115 lines (107 loc) · 6.45 KB
// 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); };
}