// 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); }; }