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