upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/scripts/build-explorer.py
diff options
context:
space:
mode:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-11 14:13:26 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-11 14:13:26 -0300
commit1839347459759fb3b23cc747ec4368b724fee008 (patch)
treee5f6f7e13b6dd9eb260d7c75bafa19d873435c5c /scripts/build-explorer.py
parenta83fae37099cb102beb5a4c174a4abe6ef3f24a4 (diff)
tools: harden chain audit scripts
Diffstat (limited to 'scripts/build-explorer.py')
-rwxr-xr-xscripts/build-explorer.py382
1 files changed, 236 insertions, 146 deletions
diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py
index 7849e8a..0d86b3c 100755
--- a/scripts/build-explorer.py
+++ b/scripts/build-explorer.py
@@ -1,134 +1,161 @@
1#!/usr/bin/env python3 1#!/usr/bin/env python3
2# 2"""Build a static Bitflash block explorer from local blk*.dat files."""
3# Build a static Bitflash block explorer (v0) from a node's blk0001.dat.
4# Emits into <out-dir>:
5# blocks.json summary of every main-chain block (newest first)
6# block/<height>.json full detail per block (header + all txs)
7# index.html self-contained viewer (vanilla JS, same-origin fetch)
8#
9# Scope (v0, deliberately): block list, block detail, and coinbase. No mempool,
10# no per-address history. On this chain the coinbase payout key rotates every
11# block, so an address index would be nearly useless; block-level transparency is
12# what lets miners and users confirm the chain is honest.
13#
14# Usage: build-explorer.py <blk0001.dat> <out-dir>
15
16import sys, os, json, hashlib, struct
17
18MAGIC = bytes([0xbf, 0x20, 0x5c, 0xfd])
19ADDRESS_VERSION = 25 # Bitflash addresses start with "B"
20
21def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest()
22 3
4import argparse
5import hashlib
6import json
7import sys
8from pathlib import Path
9
10import bitflash_chain as chain
11
12
13ADDRESS_VERSION = 25
23B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 14B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
15
16
24def b58check(payload): 17def b58check(payload):
25 v = bytes([ADDRESS_VERSION]) + payload 18 v = bytes([ADDRESS_VERSION]) + payload
26 v += sha256d(v)[:4] 19 v += chain.sha256d(v)[:4]
27 n = int.from_bytes(v, 'big') 20 n = int.from_bytes(v, "big")
28 s = "" 21 s = ""
29 while n > 0: 22 while n > 0:
30 n, r = divmod(n, 58); s = B58[r] + s 23 n, r = divmod(n, 58)
24 s = B58[r] + s
31 for byte in v: 25 for byte in v:
32 if byte == 0: s = "1" + s 26 if byte == 0:
33 else: break 27 s = "1" + s
28 else:
29 break
34 return s 30 return s
35 31
32
36def hash160(b): 33def hash160(b):
37 return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest() 34 return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest()
38 35
39class R: 36
40 def __init__(s, b, o=0): s.b = b; s.o = o 37def spk_info(script_hex):
41 def u(s, n): v = s.b[s.o:s.o+n]; s.o += n; return v 38 spk = bytes.fromhex(script_hex)
42 def i32(s): return struct.unpack('<i', s.u(4))[0] 39 if (
43 def u32(s): return struct.unpack('<I', s.u(4))[0] 40 len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9
44 def i64(s): return struct.unpack('<q', s.u(8))[0] 41 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC
45 def cs(s): 42 ):
46 c = s.b[s.o]; s.o += 1
47 if c < 253: return c
48 if c == 253: return struct.unpack('<H', s.u(2))[0]
49 if c == 254: return struct.unpack('<I', s.u(4))[0]
50 return struct.unpack('<Q', s.u(8))[0]
51 def script(s): n = s.cs(); return s.u(n)
52
53def spk_info(spk):
54 # P2PKH: OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG
55 if len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xa9 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xac:
56 return {"type": "p2pkh", "address": b58check(spk[3:23])} 43 return {"type": "p2pkh", "address": b58check(spk[3:23])}
57 # P2PK: <pubkey> OP_CHECKSIG (65- or 33-byte key) 44 if len(spk) >= 35 and spk[-1] == 0xAC:
58 if len(spk) >= 35 and spk[-1] == 0xac:
59 if spk[0] == 0x41 and len(spk) == 67: 45 if spk[0] == 0x41 and len(spk) == 67:
60 pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} 46 pk = spk[1:66]
47 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
61 if spk[0] == 0x21 and len(spk) == 35: 48 if spk[0] == 0x21 and len(spk) == 35:
62 pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} 49 pk = spk[1:34]
63 return {"type": "other", "hex": spk.hex()[:80]} 50 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
64 51 return {"type": "other", "hex": script_hex[:80]}
65def parse_tx(r): 52
66 start = r.o 53
67 r.i32() 54def explorer_tx(tx):
68 nin = r.cs(); vin = [] 55 vin = []
69 for _ in range(nin): 56 for txin in tx["vin"]:
70 ph = r.u(32); idx = r.u32(); r.script(); r.u32() 57 coinbase = (
71 coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff) 58 txin["prevout_hash"] == "0" * 64
72 vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)}) 59 and txin["prevout_n"] == 0xFFFFFFFF
73 nout = r.cs(); vout = [] 60 )
74 for _ in range(nout): 61 vin.append({
75 val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk))) 62 "coinbase": coinbase,
76 r.u32() 63 "prev": None if coinbase else txin["prevout_hash"],
77 raw = r.b[start:r.o] 64 "vout": None if coinbase else txin["prevout_n"],
78 return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)} 65 })
79 66 vout = []
80def main(): 67 for out in tx["vout"]:
81 if len(sys.argv) != 3: 68 vout.append(dict({"value": out["value_satoshis"]}, **spk_info(out["script_pub_key_hex"])))
82 print("usage: build-explorer.py <blk0001.dat> <out-dir>", file=sys.stderr); sys.exit(2) 69 return {"txid": tx["txid"], "vin": vin, "vout": vout, "size": len(chain.ser_tx(tx))}
83 blkpath, out = sys.argv[1], sys.argv[2] 70
84 data = open(blkpath, 'rb').read() 71
85 blocks = {}; o = 0; n = len(data) 72def parse_args(argv):
86 while o + 8 <= n: 73 ap = argparse.ArgumentParser(description=__doc__)
87 if data[o:o+4] != MAGIC: o += 1; continue 74 ap.add_argument("--datadir", help="Bitflash data directory containing blk*.dat")
88 o += 4; size = struct.unpack('<I', data[o:o+4])[0]; o += 4 75 ap.add_argument("--max-blocks", type=int, default=0,
89 blk = data[o:o+size]; o += size 76 help="number of blocks to scan; 0 means all block files")
90 if len(blk) < 80: continue 77 ap.add_argument("paths", nargs="+",
91 h = sha256d(blk[:80])[::-1].hex() 78 help="with --datadir: <out-dir>; otherwise: <blk*.dat>... <out-dir>")
92 r = R(blk, 0) 79 args = ap.parse_args(argv)
93 ver = r.i32(); prev = r.u(32)[::-1].hex(); merk = r.u(32)[::-1].hex() 80 if args.datadir:
94 t = r.u32(); bits = r.u32(); nonce = r.u32() 81 if len(args.paths) != 1:
95 ntx = r.cs(); txs = [] 82 raise SystemExit("usage with --datadir: build-explorer.py --datadir DATADIR <out-dir>")
96 try: 83 args.block_files = None
97 for _ in range(ntx): txs.append(parse_tx(r)) 84 args.out_dir = args.paths[0]
98 except Exception: 85 else:
99 continue 86 if len(args.paths) < 2:
100 blocks[h] = {"hash": h, "prev": prev, "merkleRoot": merk, "version": ver, 87 raise SystemExit("usage: build-explorer.py <blk*.dat>... <out-dir>")
101 "time": t, "bits": bits, "nonce": nonce, "size": size, "txs": txs} 88 args.block_files = args.paths[:-1]
102 if not blocks: 89 args.out_dir = args.paths[-1]
103 print("no blocks parsed", file=sys.stderr); sys.exit(1) 90 return args
104 91
105 hc = {} 92
106 def height(h): 93def load_blocks(args):
107 chain = []; cur = h 94 if args.datadir:
108 while cur in blocks and cur not in hc: 95 return chain.read_blocks(args.datadir, args.max_blocks)
109 chain.append(cur); cur = blocks[cur]["prev"] 96 return chain.read_blocks_from_files(args.block_files, args.max_blocks)
110 base = hc.get(cur, 0) 97
111 for hh in reversed(chain): base += 1; hc[hh] = base 98
112 return hc[h] 99def build_explorer(raw_blocks, out_dir):
113 for h in blocks: height(h) 100 try:
114 tip = max(blocks, key=lambda h: hc[h]) 101 blocks = chain.select_main_chain(raw_blocks)
115 chain = []; cur = tip 102 except chain.ParseError as e:
116 while cur in blocks: chain.append(cur); cur = blocks[cur]["prev"] 103 raise SystemExit(str(e))
117 chain.reverse() 104 out = Path(out_dir)
118 105 (out / "block").mkdir(parents=True, exist_ok=True)
119 os.makedirs(os.path.join(out, "block"), exist_ok=True) 106
120 summaries = [] 107 summaries = []
121 for h in chain: 108 for height, block in enumerate(blocks):
122 b = blocks[h]; H = hc[h] 109 txs = [explorer_tx(tx) for tx in block["transactions"]]
123 cb = b["txs"][0]["vout"][0] if b["txs"] and b["txs"][0]["vout"] else {} 110 detail = {
124 summaries.append({"height": H, "hash": h, "time": b["time"], "nTx": len(b["txs"]), 111 "height": height,
125 "size": b["size"], "cbValue": cb.get("value", 0), "cbAddr": cb.get("address", "")}) 112 "hash": block["hash"],
126 json.dump(dict({"height": H}, **b), open(os.path.join(out, "block", "%d.json" % H), "w"), separators=(',', ':')) 113 "prev": block["previous_hash"],
114 "merkleRoot": block["merkle_root"],
115 "version": block["version"],
116 "time": block["time"],
117 "bits": int(block["bits"], 16),
118 "nonce": block["nonce"],
119 "size": block["size"],
120 "txs": txs,
121 }
122 cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {}
123 summaries.append({
124 "height": height,
125 "hash": block["hash"],
126 "time": block["time"],
127 "nTx": len(txs),
128 "size": block["size"],
129 "cbValue": cb.get("value", 0),
130 "cbAddr": cb.get("address", ""),
131 })
132 (out / "block" / ("%d.json" % height)).write_text(
133 json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n",
134 encoding="ascii",
135 )
136
127 summaries.sort(key=lambda x: -x["height"]) 137 summaries.sort(key=lambda x: -x["height"])
128 json.dump({"network": "bitflash", "tipHeight": hc[tip], "count": len(chain), "blocks": summaries}, 138 (out / "blocks.json").write_text(
129 open(os.path.join(out, "blocks.json"), "w"), separators=(',', ':')) 139 json.dumps({
130 open(os.path.join(out, "index.html"), "w").write(INDEX_HTML) 140 "network": "bitflash",
131 print("explorer built: %d main-chain blocks, tip %d, into %s" % (len(chain), hc[tip], out)) 141 "tipHeight": len(blocks) - 1,
142 "count": len(blocks),
143 "blocks": summaries,
144 }, sort_keys=True, separators=(",", ":")) + "\n",
145 encoding="ascii",
146 )
147 (out / "index.html").write_text(INDEX_HTML, encoding="utf-8")
148 return len(blocks), len(blocks) - 1
149
150
151def main(argv):
152 args = parse_args(argv)
153 raw_blocks = load_blocks(args)
154 if not raw_blocks:
155 raise SystemExit("no Bitflash blocks parsed")
156 count, tip = build_explorer(raw_blocks, args.out_dir)
157 print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir))
158 return 0
132 159
133 160
134INDEX_HTML = r"""<!doctype html> 161INDEX_HTML = r"""<!doctype html>
@@ -144,6 +171,7 @@ INDEX_HTML = r"""<!doctype html>
144 body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); } 171 body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); }
145 header { padding:20px 24px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline; flex-wrap:wrap; } 172 header { padding:20px 24px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline; flex-wrap:wrap; }
146 h1 { font-size:18px; margin:0; } 173 h1 { font-size:18px; margin:0; }
174 h2 { font-size:16px; }
147 .mut { color:var(--mut); } 175 .mut { color:var(--mut); }
148 .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; } 176 .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; }
149 input { width:100%; max-width:420px; padding:9px 12px; border:1px solid var(--line); border-radius:8px; background:var(--card); color:var(--fg); font-size:14px; } 177 input { width:100%; max-width:420px; padding:9px 12px; border:1px solid var(--line); border-radius:8px; background:var(--card); color:var(--fg); font-size:14px; }
@@ -154,7 +182,6 @@ INDEX_HTML = r"""<!doctype html>
154 tr.blk:hover td { background:var(--card); } 182 tr.blk:hover td { background:var(--card); }
155 code { font-size:12px; word-break:break-all; } 183 code { font-size:12px; word-break:break-all; }
156 .scroll { overflow-x:auto; } 184 .scroll { overflow-x:auto; }
157 a { color:var(--acc); text-decoration:none; }
158 .card { border:1px solid var(--line); border-radius:10px; background:var(--card); padding:14px 16px; margin-top:12px; } 185 .card { border:1px solid var(--line); border-radius:10px; background:var(--card); padding:14px 16px; margin-top:12px; }
159 .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; } 186 .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; }
160 .kv div:nth-child(odd){ color:var(--mut); } 187 .kv div:nth-child(odd){ color:var(--mut); }
@@ -174,7 +201,7 @@ INDEX_HTML = r"""<!doctype html>
174 <button class="link" id="home" style="display:none">back to blocks</button> 201 <button class="link" id="home" style="display:none">back to blocks</button>
175</header> 202</header>
176<div class="wrap"> 203<div class="wrap">
177 <p class="note">Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here. On Bitflash the coinbase payout key rotates every block, so an address index would say almost nothing; what matters is that every block and every coinbase is here to check.</p> 204 <p class="note">Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.</p>
178 <div id="listview"> 205 <div id="listview">
179 <input id="q" placeholder="Search by height or block hash"> 206 <input id="q" placeholder="Search by height or block hash">
180 <div class="scroll"><table> 207 <div class="scroll"><table>
@@ -186,52 +213,114 @@ INDEX_HTML = r"""<!doctype html>
186 <div id="detail" style="display:none"></div> 213 <div id="detail" style="display:none"></div>
187</div> 214</div>
188<script> 215<script>
189var SUM = [], BYH = {}, SHOWN = 200; 216var SUM = [], SHOWN = 200;
190function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } 217function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; }
191function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); } 218function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); }
192function esc(s){ return String(s).replace(/[&<>]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;'}[c];}); } 219function short(h){ return h ? h.slice(0,10)+"..."+h.slice(-6) : "-"; }
193function short(h){ return h ? h.slice(0,10)+"…"+h.slice(-6) : "-"; } 220function E(tag, cls, text){
194 221 var e = document.createElement(tag);
222 if(cls) e.className = cls;
223 if(text !== undefined) e.textContent = text;
224 return e;
225}
226function code(text){ return E('code', '', text); }
227function clear(e){ while(e.firstChild) e.removeChild(e.firstChild); }
228function append(parent){
229 for(var i=1;i<arguments.length;i++) parent.appendChild(arguments[i]);
230 return parent;
231}
232function row(cells, cls){
233 var tr = E('tr', cls || '');
234 cells.forEach(function(c){ append(tr, E('td', '', c)); });
235 return tr;
236}
195function renderList(filter){ 237function renderList(filter){
196 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows'), out=""; 238 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows');
239 clear(rows);
197 var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN); 240 var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN);
198 for(var i=0;i<list.length;i++){ var b=list[i]; 241 if(!list.length){
199 out += '<tr class="blk" data-h="'+b.height+'"><td>'+b.height+'</td><td>'+ts(b.time)+'</td><td>'+b.nTx+'</td><td>'+b.size+' B</td><td>'+btf(b.cbValue)+'</td><td><code>'+(b.cbAddr?esc(b.cbAddr):'-')+'</code></td></tr>'; 242 var empty = E('tr');
243 var td = E('td', 'mut', 'no match');
244 td.colSpan = 6;
245 append(empty, td); append(rows, empty);
200 } 246 }
201 rows.innerHTML = out || '<tr><td colspan="6" class="mut">no match</td></tr>'; 247 list.forEach(function(b){
248 var tr = row([String(b.height), ts(b.time), String(b.nTx), b.size+' B', btf(b.cbValue), ''], 'blk');
249 tr.dataset.h = b.height;
250 clear(tr.children[5]);
251 append(tr.children[5], code(b.cbAddr || '-'));
252 tr.onclick = function(){ openBlock(tr.dataset.h); };
253 append(rows, tr);
254 });
202 document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : ""; 255 document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : "";
203 Array.prototype.forEach.call(rows.querySelectorAll('tr.blk'), function(tr){ tr.onclick=function(){ openBlock(tr.getAttribute('data-h')); }; });
204} 256}
205function ioSide(title, items, html){ 257function kv(parent, key, value, isCode){
206 var s='<div><div class="mut">'+title+'</div>'; for(var i=0;i<items.length;i++){ s+=html(items[i]); } return s+'</div>'; 258 append(parent, E('div', '', key));
259 append(parent, isCode ? append(E('div'), code(value)) : E('div', '', value));
260}
261function ioSide(title, items, render){
262 var side = E('div');
263 append(side, E('div', 'mut', title));
264 items.forEach(function(item){ append(side, render(item)); });
265 return side;
207} 266}
208function openBlock(h){ 267function openBlock(h){
209 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){ 268 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){
210 var s='<div class="card"><div class="kv">'+ 269 var detail = document.getElementById('detail');
211 '<div>Height</div><div>'+b.height+'</div>'+ 270 clear(detail);
212 '<div>Hash</div><div><code>'+esc(b.hash)+'</code></div>'+ 271 append(detail, E('h2', '', 'Block '+b.height));
213 '<div>Previous</div><div><button class="link" onclick="openBlock('+(b.height-1)+')"><code>'+esc(short(b.prev))+'</code></button></div>'+ 272 var card = E('div', 'card'), grid = E('div', 'kv');
214 '<div>Merkle root</div><div><code>'+esc(b.merkleRoot)+'</code></div>'+ 273 kv(grid, 'Height', String(b.height));
215 '<div>Time (UTC)</div><div>'+ts(b.time)+'</div>'+ 274 kv(grid, 'Hash', b.hash, true);
216 '<div>Bits / Nonce</div><div>'+b.bits.toString(16)+' / '+b.nonce+'</div>'+ 275 var prev = E('div');
217 '<div>Size / Txs</div><div>'+b.size+' B / '+b.txs.length+'</div>'+ 276 if(b.height > 0){
218 '</div></div>'; 277 var btn = E('button', 'link');
219 for(var i=0;i<b.txs.length;i++){ var t=b.txs[i]; 278 append(btn, code(short(b.prev)));
220 s+='<div class="tx"><div><code>'+esc(t.txid)+'</code> '+(i===0?'<span class="pill">coinbase</span>':'')+'</div><div class="io">'; 279 btn.onclick = function(){ openBlock(b.height - 1); };
221 s+=ioSide('Inputs', t.vin, function(v){ return '<div>'+(v.coinbase?'<span class="pill">coinbase</span>':'<code>'+esc(short(v.prev))+':'+v.vout+'</code>')+'</div>'; }); 280 append(prev, btn);
222 s+=ioSide('Outputs', t.vout, function(o){ var to=o.address?esc(o.address):(o.pubkey?'pubkey '+esc(o.pubkey.slice(0,16))+'…':esc(o.type)); return '<div>'+btf(o.value)+' → <code>'+to+'</code></div>'; }); 281 } else {
223 s+='</div></div>'; 282 append(prev, code(short(b.prev)));
224 } 283 }
284 append(grid, E('div', '', 'Previous'), prev);
285 kv(grid, 'Merkle root', b.merkleRoot, true);
286 kv(grid, 'Time (UTC)', ts(b.time));
287 kv(grid, 'Bits / Nonce', b.bits.toString(16)+' / '+b.nonce);
288 kv(grid, 'Size / Txs', b.size+' B / '+b.txs.length);
289 append(card, grid); append(detail, card);
290 b.txs.forEach(function(t, i){
291 var tx = E('div', 'tx');
292 var head = E('div');
293 append(head, code(t.txid));
294 if(i === 0) append(head, E('span', 'pill', 'coinbase'));
295 var io = E('div', 'io');
296 append(io, ioSide('Inputs', t.vin, function(v){
297 var d = E('div');
298 if(v.coinbase) append(d, E('span', 'pill', 'coinbase'));
299 else append(d, code(short(v.prev)+':'+v.vout));
300 return d;
301 }));
302 append(io, ioSide('Outputs', t.vout, function(o){
303 var d = E('div');
304 var to = o.address ? o.address : (o.pubkey ? 'pubkey '+o.pubkey.slice(0,16)+'...' : o.type);
305 append(d, document.createTextNode(btf(o.value)+' -> '), code(to));
306 return d;
307 }));
308 append(tx, head, io); append(detail, tx);
309 });
225 document.getElementById('listview').style.display='none'; 310 document.getElementById('listview').style.display='none';
226 var d=document.getElementById('detail'); d.style.display='block'; d.innerHTML='<h2 style="font-size:16px">Block '+b.height+'</h2>'+s; 311 detail.style.display='block';
227 document.getElementById('home').style.display='inline'; 312 document.getElementById('home').style.display='inline';
228 window.scrollTo(0,0); 313 window.scrollTo(0,0);
229 }); 314 });
230} 315}
231document.getElementById('home').onclick=function(){ document.getElementById('detail').style.display='none'; document.getElementById('listview').style.display='block'; this.style.display='none'; }; 316document.getElementById('home').onclick=function(){
317 document.getElementById('detail').style.display='none';
318 document.getElementById('listview').style.display='block';
319 this.style.display='none';
320};
232document.getElementById('q').oninput=function(){ renderList(this.value); }; 321document.getElementById('q').oninput=function(){ renderList(this.value); };
233fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ 322fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
234 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' · '+d.count+' blocks'; 323 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks';
235 renderList(''); 324 renderList('');
236}); 325});
237</script> 326</script>
@@ -239,5 +328,6 @@ fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
239</html> 328</html>
240""" 329"""
241 330
331
242if __name__ == "__main__": 332if __name__ == "__main__":
243 main() 333 raise SystemExit(main(sys.argv[1:]))