// Pulse: New Pairs, Final Stretch and Migrated, live. import { h, $, age, usd, compact, count, copy, store, toast } from './util.js'; import { icon, iconEl } from './icons.js'; import { live, net } from './net.js'; import { paper } from './paper.js'; import { avatar, setImg, socials, level, chip, emptyState } from './parts.js'; import { go } from './router.js'; const COLS = [ { key: 'new', title: 'New Pairs', empty: ['Watching for launches', 'New coins appear here the second they are created.'] }, { key: 'stretch', title: 'Final Stretch', empty: ['Nothing close to migrating', 'Coins that sell most of their curve show up here.'] }, { key: 'migrated', title: 'Migrated', empty: ['No migrations yet', 'Coins land here the moment their curve completes.'] }, ]; const SHOW = 60; const DEF_FILTER = { mcMin: '', mcMax: '', hMin: '', top10Max: '', devMax: '', snMax: '', buMax: '', ageMax: '', socials: false, tw: false, web: false, tg: false, kw: '' }; export function pulseView(root) { const rows = new Map(); const hidden = new Set(store.get('hidden', [])); const st = {}; const grid = h('div.pulse.page-in'); let first = true, raf = 0; for (const c of COLS) { const s = st[c.key] = { order: [], els: new Map(), hold: false, filter: { ...DEF_FILTER, ...store.get('filter.' + c.key, {}) }, qb: store.get('qb.' + c.key, '0.1'), }; const qbInput = h('input', { value: s.qb, inputMode: 'decimal', title: 'Quick buy amount (SOL)', oninput: e => { s.qb = e.target.value; store.set('qb.' + c.key, s.qb); } }); const fbtn = h('button.icon-btn', { title: 'Filters', html: icon('filter', 15), onclick: e => openFilter(c, s, e.currentTarget) }); s.fbtn = fbtn; s.count = h('span.count', null, '0'); s.list = h('div.list'); s.emptyEl = h('div.col-empty', null, emptyState(...c.empty)); s.col = h('section.col', null, h('div.col-h', null, h('h2', null, c.title), s.count, h('span.paused', { html: icon('pause', 11) + 'Paused' }), h('div.col-tools', null, h('label.qb-set', { title: 'Quick buy amount (SOL)' }, iconEl('bolt', 13), qbInput, h('span.qb-unit', null, 'SOL')), fbtn)), s.list); s.list.addEventListener('mouseenter', () => { s.hold = true; s.col.classList.add('hold'); }); s.list.addEventListener('mouseleave', () => { s.hold = false; s.col.classList.remove('hold'); schedule(); }); markFilter(s); grid.append(s.col); } root.append(grid); function passes(r, f) { const sol = live.sol || 0; const mcUsd = r.mc * sol; if (f.mcMin !== '' && mcUsd < +f.mcMin * 1000) return false; if (f.mcMax !== '' && mcUsd > +f.mcMax * 1000) return false; if (f.hMin !== '' && r.h < +f.hMin) return false; if (f.top10Max !== '' && r.top10 > +f.top10Max) return false; if (f.devMax !== '' && r.dh > +f.devMax) return false; if (f.snMax !== '' && r.sn > +f.snMax) return false; if (f.buMax !== '' && r.bu > +f.buMax) return false; if (f.ageMax !== '' && r.t && Date.now() - r.t > +f.ageMax * 60e3) return false; if (f.socials && !r.tw && !r.web && !r.tg) return false; if (f.tw && !r.tw) return false; if (f.web && !r.web) return false; if (f.tg && !r.tg) return false; if (f.kw) { const words = f.kw.toLowerCase().split(/[\s,]+/).filter(Boolean); const text = (r.n + ' ' + r.s).toLowerCase(); if (words.length && !words.some(w => text.includes(w))) return false; } return true; } function render() { raf = 0; for (const c of COLS) { const s = st[c.key]; const want = []; for (const m of s.order) { const r = rows.get(m); if (!r || hidden.has(m) || !passes(r, s.filter)) continue; want.push(r); if (want.length >= SHOW) break; } s.count.textContent = want.length; for (const r of want) { let el = s.els.get(r.m); if (!el) { el = rowEl(r, c.key); s.els.set(r.m, el); if (!first) el.classList.add('new'); } else if (el._v !== r.v) updateRow(el, r); } if (s.hold) continue; if (!want.length) { if (s.list.firstChild !== s.emptyEl) { s.list.replaceChildren(s.emptyEl); s.els.clear(); } continue; } if (s.emptyEl.parentNode) s.emptyEl.remove(); const keep = new Set(want.map(r => r.m)); for (const [m, el] of s.els) if (!keep.has(m)) { el.remove(); s.els.delete(m); } let i = 0; for (const r of want) { const el = s.els.get(r.m); const at = s.list.children[i]; if (at !== el) s.list.insertBefore(el, at || null); i++; } } if (rows.size) first = false; } function schedule() { if (!raf) raf = requestAnimationFrame(render); } function rowEl(r, col) { const ring = h('div.ring'); const av = avatar(r, 64); const badge = h('span.badge'); ring.append(av, badge); const nm = h('span.nm'), sy = h('span.sy'); const cp = h('button.cp', { title: 'Copy address', html: icon('copy', 13), onclick: e => { e.preventDefault(); e.stopPropagation(); copy(r.m, 'Address copied'); } }); const agev = h('span.agev'); const soc = h('div.soc'); const hc = h('span.hc'); const l3 = h('div.l3'); const mc = h('span.v'), vol = h('span.v.sm'), txn = h('span'), bar = h('span.bar', null, h('i')); const qb = h('button.qb', { title: 'Paper buy', onclick: e => { e.preventDefault(); e.stopPropagation(); quickBuy(el._r, col); } }); const hide = h('button.hide-btn', { title: 'Hide this coin', html: icon('eyeoff', 13), onclick: e => { e.preventDefault(); e.stopPropagation(); hideCoin(el._r.m); } }); const el = h('a.row', { href: '/coin/' + r.m, 'data-link': '' }, ring, h('div.mid', null, h('div.l1', null, nm, sy, cp), h('div.l2', null, agev, soc, hc), l3), h('div.right', null, h('div.stat', null, h('span.k', null, 'MC'), mc), h('div.stat', null, h('span.k', null, 'V'), vol), h('div.txbar', null, h('span.k.mut', { style: { fontSize: '11px', fontWeight: 600, color: 'var(--tx4)' } }, 'TX'), txn, bar), qb), hide); Object.assign(el, { _ring: ring, _av: av, _badge: badge, _nm: nm, _sy: sy, _age: agev, _soc: soc, _hc: hc, _l3: l3, _mc: mc, _vol: vol, _txn: txn, _bar: bar, _qb: qb, _col: col }); updateRow(el, r); return el; } function updateRow(el, r) { const prev = el._r; el._r = r; el._v = r.v; if (!prev || prev.n !== r.n) { el._nm.textContent = r.n || r.s || '–'; el.title = r.n || ''; } if (!prev || prev.s !== r.s) el._sy.textContent = r.s; if (r.img) setImg(el._av, r.img); const mig = !!r.mig || r.pool === 'pump-amm'; el._ring.classList.toggle('mig', mig); el._ring.style.setProperty('--p', mig ? 100 : r.pr); if (!prev || (prev.pool !== r.pool)) { el._badge.innerHTML = icon(mig ? 'swap' : 'pill', 10); el._badge.title = mig ? 'PumpSwap' : 'pump.fun curve'; } el._badge.title = mig ? 'PumpSwap' : `Bonding curve ${r.pr.toFixed(1)}%`; const ts = el._col === 'migrated' ? r.mig : r.t; el._age.dataset.t = ts || ''; el._age.textContent = ts ? age(ts) : ''; if (!prev || prev.tw !== r.tw || prev.web !== r.web || prev.tg !== r.tg) el._soc.replaceWith(el._soc = socials(r, 14)); const seen = !!r.t && r.top10 != null; el._hc.replaceChildren(iconEl('users', 13), h('span', null, r.t ? String(r.h) : '–')); el._hc.title = r.t ? 'Holders' : 'Holders: this coin launched before OScope started watching'; if (!seen) el._l3.replaceChildren( chip('crown', '–', '', 'Top 10 holders: not known yet'), chip('chef', '–', '', 'Dev holds: not known yet'), chip('target', '–', '', 'Snipers: not known yet'), chip('boxes', '–', '', 'Bundle: not known yet')); else el._l3.replaceChildren( chip('crown', r.top10.toFixed(0) + '%', level(r.top10, 20, 40), 'Top 10 holders'), chip('chef', (r.ds ? 'Sold' : r.dh.toFixed(r.dh < 10 ? 1 : 0) + '%'), r.ds ? 'lv-m' : level(r.dh, 5, 15), r.ds ? 'Dev sold everything' : 'Dev holds'), chip('target', r.sn.toFixed(0) + '%', level(r.sn, 3, 15), 'Snipers hold'), chip('boxes', r.bu.toFixed(0) + '%', level(r.bu, 5, 20), 'Bundle holds')); const sol = live.sol || 0; const mcText = usd(r.mc * sol); if (el._mc.textContent !== mcText) { if (prev && prev.mc !== r.mc) { el._mc.classList.remove('fl-up', 'fl-down'); el._mc.classList.add(r.mc > prev.mc ? 'fl-up' : 'fl-down'); clearTimeout(el._fl); el._fl = setTimeout(() => el._mc.classList.remove('fl-up', 'fl-down'), 650); } el._mc.textContent = mcText; } el._vol.textContent = usd(r.vol * sol); const n = r.buys + r.sells; el._txn.textContent = count(n); el._bar.firstChild.style.width = (n ? (r.buys / n) * 100 : 50) + '%'; el._bar.title = `${r.buys} buys · ${r.sells} sells`; const amt = st[el._col].qb; el._qb.replaceChildren(iconEl('bolt', 12), h('span', null, amt || '0')); } function quickBuy(r, col) { const amt = parseFloat(st[col].qb); paper.learn(r.m, { rq: r.rq, rt: r.rt, fee: r.fee, price: r.p, mc: r.mc, pool: r.pool, n: r.n, s: r.s, img: r.img }); paper.buy(r.m, amt, { n: r.n, s: r.s, img: r.img }); } function hideCoin(m) { hidden.add(m); store.set('hidden', [...hidden].slice(-3000)); toast('Coin hidden. Settings → Hidden coins brings it back.', 'info', 2200); schedule(); } function markFilter(s) { const on = Object.entries(s.filter).some(([k, v]) => v !== DEF_FILTER[k]); s.fbtn.classList.toggle('on', on); } let pop = null; function closePop() { if (pop) { pop.remove(); pop = null; document.removeEventListener('mousedown', outside, true); } } function outside(e) { if (pop && !pop.contains(e.target)) closePop(); } function openFilter(c, s, btn) { if (pop) { const same = pop._col === c.key; closePop(); if (same) return; } const f = s.filter; const num = (k, ph) => h('input.inp', { value: f[k], placeholder: ph, inputMode: 'decimal', oninput: e => set(k, e.target.value.trim()) }); const ck = (k, label, ic) => { const b = h('button.ck' + (f[k] ? '.on' : ''), { onclick: () => { set(k, !f[k]); b.classList.toggle('on', f[k]); } }, ic ? iconEl(ic, 12) : null, label); return b; }; function set(k, v) { f[k] = v; store.set('filter.' + c.key, f); markFilter(s); schedule(); } pop = h('div.pop', null, h('h4', null, c.title + ' filters', h('button.btn.ghost.sm', { onclick: () => { Object.assign(f, DEF_FILTER); store.set('filter.' + c.key, f); markFilter(s); schedule(); closePop(); } }, 'Reset')), h('div.field', null, h('span.lbl', null, 'Market cap, $K'), h('div.two', null, num('mcMin', 'Min'), num('mcMax', 'Max'))), h('div.two', null, h('div.field', null, h('span.lbl', null, 'Holders, min'), num('hMin', '0')), h('div.field', null, h('span.lbl', null, 'Age, max minutes'), num('ageMax', 'Any'))), h('div.two', null, h('div.field', null, h('span.lbl', null, 'Top 10, max %'), num('top10Max', 'Any')), h('div.field', null, h('span.lbl', null, 'Dev holds, max %'), num('devMax', 'Any'))), h('div.two', null, h('div.field', null, h('span.lbl', null, 'Snipers, max %'), num('snMax', 'Any')), h('div.field', null, h('span.lbl', null, 'Bundle, max %'), num('buMax', 'Any'))), h('div.field', null, h('span.lbl', null, 'Has'), h('div.checks', null, ck('socials', 'Any social'), ck('tw', 'X', 'xlogo'), ck('web', 'Website', 'globe'), ck('tg', 'Telegram', 'tg'))), h('div.field', null, h('span.lbl', null, 'Name contains (any of)'), h('input.inp', { value: f.kw, placeholder: 'cat, dog, ai', oninput: e => set('kw', e.target.value) }))); pop._col = c.key; const r = btn.getBoundingClientRect(); pop.style.top = r.bottom + 8 + 'px'; pop.style.left = Math.min(window.innerWidth - 316, Math.max(8, r.right - 300)) + 'px'; document.body.append(pop); setTimeout(() => document.addEventListener('mousedown', outside, true)); } const offP = live.on('p', m => { for (const r of m.rows) rows.set(r.m, r); if (m.cols) { for (const c of COLS) st[c.key].order = m.cols[c.key] || []; const keep = new Set([...(m.cols.new || []), ...(m.cols.stretch || []), ...(m.cols.migrated || [])]); for (const k of rows.keys()) if (!keep.has(k)) rows.delete(k); } schedule(); }); const tick = setInterval(() => { const t = Date.now(); for (const c of COLS) for (const el of st[c.key].els.values()) { const ts = +el._age.dataset.t; if (ts) { const a = age(ts, t); if (el._age.textContent !== a) el._age.textContent = a; el._age.classList.toggle('old', t - ts > 3600e3); } } }, 1000); net.pulse(true); return () => { net.pulse(false); offP(); clearInterval(tick); closePop(); if (raf) cancelAnimationFrame(raf); }; }