Back to OScope
oscope/public/js/trackerview.js
Code
111 lines (107 loc) · 7.33 KB
// Trackers: the wallets you follow and their trades, live. Wallet page: what one wallet holds among the coins OScope has seen.
import { h, age, usd, sol as fsol, compact, short, copy, signed, toast } from './util.js';
import { icon, iconEl } from './icons.js';
import { live, net } from './net.js';
import { trackers } from './trackers.js';
import { avatar, emptyState } from './parts.js';
export function trackersView(root) {
const wrap = h('div.wrap');
root.append(h('div.page.page-in', null, wrap));
const listBox = h('div.trk-list');
const feedBody = h('div');
const addr = h('input.inp', { placeholder: 'Wallet address', spellcheck: false });
const name = h('input.inp', { placeholder: 'Name' });
const add = () => { if (trackers.add(addr.value, name.value)) { addr.value = ''; name.value = ''; } };
addr.addEventListener('keydown', e => { if (e.key === 'Enter') add(); });
name.addEventListener('keydown', e => { if (e.key === 'Enter') add(); });
const fileIn = h('input', { type: 'file', accept: '.json,application/json', style: { display: 'none' }, onchange: async e => { const n = trackers.importJson(await e.target.files[0].text()); toast(`${n} wallets added`, 'info'); e.target.value = ''; } });
wrap.append(
h('div.ph-h', null, h('div', null, h('h1', null, 'Trackers'), h('p', null, 'Name any wallet and see its trades as they land, on every page.'))),
h('div.trk', null,
h('div.panel', null,
h('div.panel-h', null, h('h3', null, 'Wallets'), h('div', { style: { display: 'flex', gap: '4px' } },
h('button.btn.sm.ghost', { onclick: () => fileIn.click() }, 'Import'),
h('button.btn.sm.ghost', { onclick: () => exportList() }, 'Export'), fileIn)),
h('div.add-form', null, addr, h('div.two', null, name, h('button.btn.acc', { onclick: add, html: icon('plus', 14) + 'Track' }))),
listBox),
h('div.panel', null, h('div.panel-h', null, h('h3', null, 'Live trades'), h('span.mut', { style: { fontSize: '12px' } }, 'This session')), feedBody)));
function drawList() {
if (!trackers.list.length) { listBox.replaceChildren(emptyState('No wallets yet', 'Paste an address above, or import a list.')); return; }
listBox.replaceChildren(...trackers.list.map(t => h('div.tw', null,
h('div', { style: { minWidth: 0 } },
h('a.nmw', { href: `/wallet/${t.w}`, 'data-link': '' }, t.emoji || '', t.name),
h('span.addr', { onclick: () => copy(t.w, 'Address copied') }, short(t.w, 6), iconEl('copy', 11))),
h('div.acts', null,
h('button.icon-btn', { title: t.alerts ? 'Pop-ups on' : 'Pop-ups off', html: icon(t.alerts ? 'bell' : 'bellOff', 15), onclick: () => trackers.update(t.w, { alerts: !t.alerts }) }),
h('button.icon-btn', { title: 'Rename', html: icon('edit', 15), onclick: () => { const n = prompt('Name for this wallet', t.name); if (n != null) trackers.update(t.w, { name: n.trim().slice(0, 32) || t.name }); } }),
h('button.icon-btn', { title: 'Stop tracking', html: icon('trash', 15), onclick: () => trackers.remove(t.w) })))));
}
function exportList() {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(trackers.list.map(({ w, name, emoji }) => ({ w, name, emoji })), null, 1)], { type: 'application/json' }));
a.download = 'oscope-wallets.json';
a.click();
}
function feedRow(it, fresh) {
const sol = live.sol || 0;
return h('tr.feed-row' + (fresh ? '.fresh' : ''), null,
h('td.mut', { 'data-t': it.ts }, age(it.ts)),
h('td', null, h('b', null, (it.who?.emoji ? it.who.emoji + ' ' : '') + (it.who?.name || short(it.w)))),
h('td', null, h('b.' + (it.buy ? 'up' : 'down'), null, it.buy ? 'Buy' : 'Sell')),
h('td', null, h('a.coin-cell', { href: `/coin/${it.m}`, 'data-link': '' }, avatar({ m: it.m, s: it.s, img: it.img }, 30), h('b', null, it.s || short(it.m)))),
h('td.r', null, fsol(it.sol) + ' SOL'),
h('td.r.mut', null, usd(it.sol * sol)),
h('td.r', null, usd(it.mc * sol)));
}
let tbody = null;
function drawFeed() {
if (!trackers.feed.length) { tbody = null; feedBody.replaceChildren(emptyState('Waiting for trades', trackers.list.length ? 'Trades of your wallets appear here the moment they land.' : 'Add a wallet to start.')); return; }
tbody = h('tbody', null, trackers.feed.slice(0, 200).map(it => feedRow(it)));
feedBody.replaceChildren(h('table.t', null, h('thead', null, h('tr', null, h('th', null, 'Age'), h('th', null, 'Wallet'), h('th', null, 'Type'), h('th', null, 'Coin'), h('th.r', null, 'Amount'), h('th.r', null, 'USD'), h('th.r', null, 'MC'))), tbody));
}
drawList();
drawFeed();
const offs = [
trackers.on('change', () => { drawList(); drawFeed(); }),
trackers.on('feed', list => {
if (!tbody) return drawFeed();
for (const it of list) { if (it.who) tbody.prepend(feedRow(it, true)); }
while (tbody.children.length > 200) tbody.lastChild.remove();
}),
];
const tick = setInterval(() => { if (tbody) for (const td of tbody.querySelectorAll('td[data-t]')) td.textContent = age(+td.dataset.t); }, 1000);
return () => { offs.forEach(f => f()); clearInterval(tick); };
}
export function walletView(root, w) {
const wrap = h('div.wrap');
root.append(h('div.page.page-in', null, wrap));
const t = trackers.name(w);
const body = h('div');
const head = h('div.ph-h', null,
h('div', null, h('h1', null, t ? t.name : 'Wallet'), h('p', null, h('span.addr', { onclick: () => copy(w, 'Address copied') }, w, iconEl('copy', 12)))),
h('div', { style: { display: 'flex', gap: '8px' } },
t ? null : h('button.btn.acc', { onclick: e => { if (trackers.add(w, '')) e.currentTarget.remove(); }, html: icon('plus', 14) + 'Track' }),
h('a.btn', { href: `https://solscan.io/account/${w}`, target: '_blank', rel: 'noopener noreferrer', html: 'Solscan ' + icon('ext', 12) })));
wrap.append(head, h('div.panel', null, h('div.panel-h', null, h('h3', null, 'Coins traded'), h('span.mut', { style: { fontSize: '12px' } }, 'Among coins OScope has seen')), body));
let alive = true;
async function load() {
let list = [];
try { list = await net.get(`/api/wallet/${w}`); } catch {}
if (!alive) return;
const sol = live.sol || 0;
if (!list.length) { body.replaceChildren(emptyState('No trades seen', 'Trades of this wallet show here once it trades a coin OScope is watching.')); return; }
body.replaceChildren(h('table.t', null,
h('thead', null, h('tr', null, h('th', null, 'Coin'), h('th.r', null, 'Holds'), h('th.r', null, 'Bought'), h('th.r', null, 'Sold'), h('th.r', null, 'PnL'), h('th.r', null, 'MC'), h('th.r', null, 'Last trade'))),
h('tbody', null, list.map(x => h('tr', null,
h('td', null, h('a.coin-cell', { href: `/coin/${x.m}`, 'data-link': '' }, avatar({ m: x.m, s: x.s, img: x.img }, 30), h('div', null, h('b', null, x.s || short(x.m)), h('div.mut', { style: { fontSize: '11.5px' } }, x.n)))),
h('td.r', null, x.bal > 0 ? usd(x.value * sol) : h('span.mut', null, 'Sold')),
h('td.r.up', null, fsol(x.bs) + ' SOL'),
h('td.r.down', null, fsol(x.ss) + ' SOL'),
h('td.r.' + (x.pnl >= 0 ? 'up' : 'down'), null, h('b', null, signed(x.pnl, v => fsol(v)) + ' SOL')),
h('td.r', null, usd(x.mc * sol)),
h('td.r.mut', null, age(x.last)))))));
}
load();
const iv = setInterval(load, 5000);
return () => { alive = false; clearInterval(iv); };
}