index.html raw
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="utf-8">
5 <title>nostrino</title>
6 <style>
7 :root { color-scheme: dark; }
8 body { font: 15px/1.5 system-ui, sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1rem; background: #121212; color: #e8e8e8; }
9 h1 { font-size: 1.4rem; letter-spacing: .02em; }
10 label { display: block; margin: 1rem 0 .3rem; color: #9a9a9a; font-size: .85rem; }
11 input, select, button, textarea { font: inherit; padding: .5rem .6rem; background: #1e1e1e; color: inherit; border: 1px solid #3a3a3a; border-radius: 4px; }
12 input, textarea { width: 100%; box-sizing: border-box; }
13 textarea { min-height: 9rem; font-family: ui-monospace, monospace; font-size: .85rem; }
14 .row { display: flex; gap: .5rem; align-items: end; }
15 button { cursor: pointer; }
16 pre { background: #1a1a1a; border: 1px solid #333; border-radius: 4px; padding: 1rem; overflow: auto; }
17 .hint { color: #8a8a8a; font-size: .85rem; }
18 </style>
19 </head>
20 <body>
21 <h1>nostrino</h1>
22 <p class="hint">A BIP157 light client in a dedicated worker. It syncs compact filters from a gateway over a public Nostr relay and matches scripts locally.</p>
23
24 <label for="uri">gateway URI</label>
25 <input id="uri" placeholder="nostr+chaindata://<gateway-pubkey>?relay=ws://127.0.0.1:34567&chains=regtest&v=1">
26
27 <div class="row" style="margin-top:.6rem">
28 <div>
29 <label for="chain">chain</label>
30 <select id="chain"><option>regtest</option><option>btc</option><option>testnet3</option></select>
31 </div>
32 <button id="sync">sync</button>
33 <button id="cancel">cancel</button>
34 </div>
35
36 <label for="script">script hex to match (optional)</label>
37 <div class="row">
38 <input id="script" placeholder="626c6f636b2d37">
39 <button id="match">match</button>
40 </div>
41
42 <details style="margin-top:1.5rem">
43 <summary class="hint">custom chain profile (XBT or another network)</summary>
44 <p class="hint">Paste the profile JSON. It must include the genesis header, hash, magic and proof-of-work limit.</p>
45 <textarea id="profile" placeholder='{"name":"xbt","magic":"fabfb5da","genesis_header":"...","genesis_hash":"..."}'></textarea>
46 <button id="setprofile" style="margin-top:.4rem">set profile</button>
47 </details>
48
49 <pre id="out">idle</pre>
50
51 <script>
52 const worker = new Worker('worker.js');
53 const out = document.getElementById('out');
54 worker.onmessage = (event) => {
55 const msg = event.data;
56 if (msg.type === 'status') out.textContent = JSON.stringify(msg.status, null, 2);
57 else if (msg.type === 'matches') out.textContent = 'matching heights: ' + JSON.stringify(msg.matches);
58 else if (msg.type === 'error') out.textContent = 'error: ' + msg.error;
59 };
60 document.getElementById('sync').onclick = () => {
61 const uri = document.getElementById('uri').value.trim();
62 const chain = document.getElementById('chain').value;
63 if (!uri) { out.textContent = 'enter a gateway URI'; return; }
64 worker.postMessage({ type: 'sync', uri, chain });
65 };
66 document.getElementById('cancel').onclick = () => worker.postMessage({ type: 'cancel' });
67 document.getElementById('setprofile').onclick = () => {
68 const json = document.getElementById('profile').value.trim();
69 if (json) worker.postMessage({ type: 'profile', json });
70 };
71 document.getElementById('match').onclick = () => {
72 const script = document.getElementById('script').value.trim();
73 if (script) worker.postMessage({ type: 'match', script });
74 };
75 </script>
76 </body>
77 </html>
78