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:
authorbitflash <admin@bitflash.network>2026-08-12 10:31:29 -0300
committerbitflash <admin@bitflash.network>2026-08-12 10:31:29 -0300
commitc8d798807e69ddbe55bfab8a34286ab75b786cb5 (patch)
tree81b5d44e5193f220b64097f496e314e04caf2db8 /scripts/build-explorer.py
parentc5f6b52c0d1d65f7bf8cd745a7a212bc6b2fe318 (diff)
parentd0ebe648f333def99fababd65f6b929448616d56 (diff)
test merge
# Conflicts: # src/makefile.mingw
Diffstat (limited to 'scripts/build-explorer.py')
-rwxr-xr-xscripts/build-explorer.py385
1 files changed, 239 insertions, 146 deletions
diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py
index 7849e8a..97a7f4e 100755
--- a/scripts/build-explorer.py
+++ b/scripts/build-explorer.py
@@ -1,134 +1,164 @@
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 try:
42 def i32(s): return struct.unpack('<i', s.u(4))[0] 39 spk = bytes.fromhex(script_hex)
43 def u32(s): return struct.unpack('<I', s.u(4))[0] 40 except ValueError:
44 def i64(s): return struct.unpack('<q', s.u(8))[0] 41 return {"type": "malformed", "hex": script_hex[:80]}
45 def cs(s): 42 if (
46 c = s.b[s.o]; s.o += 1 43 len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9
47 if c < 253: return c 44 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC
48 if c == 253: return struct.unpack('<H', s.u(2))[0] 45 ):
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])} 46 return {"type": "p2pkh", "address": b58check(spk[3:23])}
57 # P2PK: <pubkey> OP_CHECKSIG (65- or 33-byte key) 47 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: 48 if spk[0] == 0x41 and len(spk) == 67:
60 pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} 49 pk = spk[1:66]
50 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
61 if spk[0] == 0x21 and len(spk) == 35: 51 if spk[0] == 0x21 and len(spk) == 35:
62 pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} 52 pk = spk[1:34]
63 return {"type": "other", "hex": spk.hex()[:80]} 53 return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))}
64 54 return {"type": "other", "hex": script_hex[:80]}
65def parse_tx(r): 55
66 start = r.o 56
67 r.i32() 57def explorer_tx(tx):
68 nin = r.cs(); vin = [] 58 vin = []
69 for _ in range(nin): 59 for txin in tx["vin"]:
70 ph = r.u(32); idx = r.u32(); r.script(); r.u32() 60 coinbase = (
71 coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff) 61 txin["prevout_hash"] == "0" * 64
72 vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)}) 62 and txin["prevout_n"] == 0xFFFFFFFF
73 nout = r.cs(); vout = [] 63 )
74 for _ in range(nout): 64 vin.append({
75 val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk))) 65 "coinbase": coinbase,
76 r.u32() 66 "prev": None if coinbase else txin["prevout_hash"],
77 raw = r.b[start:r.o] 67 "vout": None if coinbase else txin["prevout_n"],
78 return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)} 68 })
79 69 vout = []
80def main(): 70 for out in tx["vout"]:
81 if len(sys.argv) != 3: 71 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) 72 return {"txid": tx["txid"], "vin": vin, "vout": vout, "size": len(chain.ser_tx(tx))}
83 blkpath, out = sys.argv[1], sys.argv[2] 73
84 data = open(blkpath, 'rb').read() 74
85 blocks = {}; o = 0; n = len(data) 75def parse_args(argv):
86 while o + 8 <= n: 76 ap = argparse.ArgumentParser(description=__doc__)
87 if data[o:o+4] != MAGIC: o += 1; continue 77 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 78 ap.add_argument("--max-blocks", type=int, default=0,
89 blk = data[o:o+size]; o += size 79 help="number of blocks to scan; 0 means all block files")
90 if len(blk) < 80: continue 80 ap.add_argument("paths", nargs="+",
91 h = sha256d(blk[:80])[::-1].hex() 81 help="with --datadir: <out-dir>; otherwise: <blk*.dat>... <out-dir>")
92 r = R(blk, 0) 82 args = ap.parse_args(argv)
93 ver = r.i32(); prev = r.u(32)[::-1].hex(); merk = r.u(32)[::-1].hex() 83 if args.datadir:
94 t = r.u32(); bits = r.u32(); nonce = r.u32() 84 if len(args.paths) != 1:
95 ntx = r.cs(); txs = [] 85 raise SystemExit("usage with --datadir: build-explorer.py --datadir DATADIR <out-dir>")
96 try: 86 args.block_files = None
97 for _ in range(ntx): txs.append(parse_tx(r)) 87 args.out_dir = args.paths[0]
98 except Exception: 88 else:
99 continue 89 if len(args.paths) < 2:
100 blocks[h] = {"hash": h, "prev": prev, "merkleRoot": merk, "version": ver, 90 raise SystemExit("usage: build-explorer.py <blk*.dat>... <out-dir>")
101 "time": t, "bits": bits, "nonce": nonce, "size": size, "txs": txs} 91 args.block_files = args.paths[:-1]
102 if not blocks: 92 args.out_dir = args.paths[-1]
103 print("no blocks parsed", file=sys.stderr); sys.exit(1) 93 return args
104 94
105 hc = {} 95
106 def height(h): 96def load_blocks(args):
107 chain = []; cur = h 97 if args.datadir:
108 while cur in blocks and cur not in hc: 98 return chain.read_blocks(args.datadir, args.max_blocks)
109 chain.append(cur); cur = blocks[cur]["prev"] 99 return chain.read_blocks_from_files(args.block_files, args.max_blocks)
110 base = hc.get(cur, 0) 100
111 for hh in reversed(chain): base += 1; hc[hh] = base 101
112 return hc[h] 102def build_explorer(raw_blocks, out_dir):
113 for h in blocks: height(h) 103 try:
114 tip = max(blocks, key=lambda h: hc[h]) 104 blocks = chain.select_main_chain(raw_blocks)
115 chain = []; cur = tip 105 except chain.ParseError as e:
116 while cur in blocks: chain.append(cur); cur = blocks[cur]["prev"] 106 raise SystemExit(str(e))
117 chain.reverse() 107 out = Path(out_dir)
118 108 (out / "block").mkdir(parents=True, exist_ok=True)
119 os.makedirs(os.path.join(out, "block"), exist_ok=True) 109
120 summaries = [] 110 summaries = []
121 for h in chain: 111 for height, block in enumerate(blocks):
122 b = blocks[h]; H = hc[h] 112 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 {} 113 detail = {
124 summaries.append({"height": H, "hash": h, "time": b["time"], "nTx": len(b["txs"]), 114 "height": height,
125 "size": b["size"], "cbValue": cb.get("value", 0), "cbAddr": cb.get("address", "")}) 115 "hash": block["hash"],
126 json.dump(dict({"height": H}, **b), open(os.path.join(out, "block", "%d.json" % H), "w"), separators=(',', ':')) 116 "prev": block["previous_hash"],
117 "merkleRoot": block["merkle_root"],
118 "version": block["version"],
119 "time": block["time"],
120 "bits": int(block["bits"], 16),
121 "nonce": block["nonce"],
122 "size": block["size"],
123 "txs": txs,
124 }
125 cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {}
126 summaries.append({
127 "height": height,
128 "hash": block["hash"],
129 "time": block["time"],
130 "nTx": len(txs),
131 "size": block["size"],
132 "cbValue": cb.get("value", 0),
133 "cbAddr": cb.get("address", ""),
134 })
135 (out / "block" / ("%d.json" % height)).write_text(
136 json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n",
137 encoding="ascii",
138 )
139
127 summaries.sort(key=lambda x: -x["height"]) 140 summaries.sort(key=lambda x: -x["height"])
128 json.dump({"network": "bitflash", "tipHeight": hc[tip], "count": len(chain), "blocks": summaries}, 141 (out / "blocks.json").write_text(
129 open(os.path.join(out, "blocks.json"), "w"), separators=(',', ':')) 142 json.dumps({
130 open(os.path.join(out, "index.html"), "w").write(INDEX_HTML) 143 "network": "bitflash",
131 print("explorer built: %d main-chain blocks, tip %d, into %s" % (len(chain), hc[tip], out)) 144 "tipHeight": len(blocks) - 1,
145 "count": len(blocks),
146 "blocks": summaries,
147 }, sort_keys=True, separators=(",", ":")) + "\n",
148 encoding="ascii",
149 )
150 (out / "index.html").write_text(INDEX_HTML, encoding="utf-8")
151 return len(blocks), len(blocks) - 1
152
153
154def main(argv):
155 args = parse_args(argv)
156 raw_blocks = load_blocks(args)
157 if not raw_blocks:
158 raise SystemExit("no Bitflash blocks parsed")
159 count, tip = build_explorer(raw_blocks, args.out_dir)
160 print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir))
161 return 0
132 162
133 163
134INDEX_HTML = r"""<!doctype html> 164INDEX_HTML = r"""<!doctype html>
@@ -144,6 +174,7 @@ INDEX_HTML = r"""<!doctype html>
144 body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); } 174 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; } 175 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; } 176 h1 { font-size:18px; margin:0; }
177 h2 { font-size:16px; }
147 .mut { color:var(--mut); } 178 .mut { color:var(--mut); }
148 .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; } 179 .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; } 180 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 +185,6 @@ INDEX_HTML = r"""<!doctype html>
154 tr.blk:hover td { background:var(--card); } 185 tr.blk:hover td { background:var(--card); }
155 code { font-size:12px; word-break:break-all; } 186 code { font-size:12px; word-break:break-all; }
156 .scroll { overflow-x:auto; } 187 .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; } 188 .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; } 189 .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; }
160 .kv div:nth-child(odd){ color:var(--mut); } 190 .kv div:nth-child(odd){ color:var(--mut); }
@@ -174,7 +204,7 @@ INDEX_HTML = r"""<!doctype html>
174 <button class="link" id="home" style="display:none">back to blocks</button> 204 <button class="link" id="home" style="display:none">back to blocks</button>
175</header> 205</header>
176<div class="wrap"> 206<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> 207 <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"> 208 <div id="listview">
179 <input id="q" placeholder="Search by height or block hash"> 209 <input id="q" placeholder="Search by height or block hash">
180 <div class="scroll"><table> 210 <div class="scroll"><table>
@@ -186,52 +216,114 @@ INDEX_HTML = r"""<!doctype html>
186 <div id="detail" style="display:none"></div> 216 <div id="detail" style="display:none"></div>
187</div> 217</div>
188<script> 218<script>
189var SUM = [], BYH = {}, SHOWN = 200; 219var SUM = [], SHOWN = 200;
190function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } 220function 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'); } 221function 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];}); } 222function short(h){ return h ? h.slice(0,10)+"..."+h.slice(-6) : "-"; }
193function short(h){ return h ? h.slice(0,10)+"…"+h.slice(-6) : "-"; } 223function E(tag, cls, text){
194 224 var e = document.createElement(tag);
225 if(cls) e.className = cls;
226 if(text !== undefined) e.textContent = text;
227 return e;
228}
229function code(text){ return E('code', '', text); }
230function clear(e){ while(e.firstChild) e.removeChild(e.firstChild); }
231function append(parent){
232 for(var i=1;i<arguments.length;i++) parent.appendChild(arguments[i]);
233 return parent;
234}
235function row(cells, cls){
236 var tr = E('tr', cls || '');
237 cells.forEach(function(c){ append(tr, E('td', '', c)); });
238 return tr;
239}
195function renderList(filter){ 240function renderList(filter){
196 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows'), out=""; 241 var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows');
242 clear(rows);
197 var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN); 243 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]; 244 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>'; 245 var empty = E('tr');
246 var td = E('td', 'mut', 'no match');
247 td.colSpan = 6;
248 append(empty, td); append(rows, empty);
200 } 249 }
201 rows.innerHTML = out || '<tr><td colspan="6" class="mut">no match</td></tr>'; 250 list.forEach(function(b){
251 var tr = row([String(b.height), ts(b.time), String(b.nTx), b.size+' B', btf(b.cbValue), ''], 'blk');
252 tr.dataset.h = b.height;
253 clear(tr.children[5]);
254 append(tr.children[5], code(b.cbAddr || '-'));
255 tr.onclick = function(){ openBlock(tr.dataset.h); };
256 append(rows, tr);
257 });
202 document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : ""; 258 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} 259}
205function ioSide(title, items, html){ 260function 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>'; 261 append(parent, E('div', '', key));
262 append(parent, isCode ? append(E('div'), code(value)) : E('div', '', value));
263}
264function ioSide(title, items, render){
265 var side = E('div');
266 append(side, E('div', 'mut', title));
267 items.forEach(function(item){ append(side, render(item)); });
268 return side;
207} 269}
208function openBlock(h){ 270function openBlock(h){
209 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){ 271 fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){
210 var s='<div class="card"><div class="kv">'+ 272 var detail = document.getElementById('detail');
211 '<div>Height</div><div>'+b.height+'</div>'+ 273 clear(detail);
212 '<div>Hash</div><div><code>'+esc(b.hash)+'</code></div>'+ 274 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>'+ 275 var card = E('div', 'card'), grid = E('div', 'kv');
214 '<div>Merkle root</div><div><code>'+esc(b.merkleRoot)+'</code></div>'+ 276 kv(grid, 'Height', String(b.height));
215 '<div>Time (UTC)</div><div>'+ts(b.time)+'</div>'+ 277 kv(grid, 'Hash', b.hash, true);
216 '<div>Bits / Nonce</div><div>'+b.bits.toString(16)+' / '+b.nonce+'</div>'+ 278 var prev = E('div');
217 '<div>Size / Txs</div><div>'+b.size+' B / '+b.txs.length+'</div>'+ 279 if(b.height > 0){
218 '</div></div>'; 280 var btn = E('button', 'link');
219 for(var i=0;i<b.txs.length;i++){ var t=b.txs[i]; 281 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">'; 282 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>'; }); 283 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>'; }); 284 } else {
223 s+='</div></div>'; 285 append(prev, code(short(b.prev)));
224 } 286 }
287 append(grid, E('div', '', 'Previous'), prev);
288 kv(grid, 'Merkle root', b.merkleRoot, true);
289 kv(grid, 'Time (UTC)', ts(b.time));
290 kv(grid, 'Bits / Nonce', b.bits.toString(16)+' / '+b.nonce);
291 kv(grid, 'Size / Txs', b.size+' B / '+b.txs.length);
292 append(card, grid); append(detail, card);
293 b.txs.forEach(function(t, i){
294 var tx = E('div', 'tx');
295 var head = E('div');
296 append(head, code(t.txid));
297 if(i === 0) append(head, E('span', 'pill', 'coinbase'));
298 var io = E('div', 'io');
299 append(io, ioSide('Inputs', t.vin, function(v){
300 var d = E('div');
301 if(v.coinbase) append(d, E('span', 'pill', 'coinbase'));
302 else append(d, code(short(v.prev)+':'+v.vout));
303 return d;
304 }));
305 append(io, ioSide('Outputs', t.vout, function(o){
306 var d = E('div');
307 var to = o.address ? o.address : (o.pubkey ? 'pubkey '+o.pubkey.slice(0,16)+'...' : o.type);
308 append(d, document.createTextNode(btf(o.value)+' -> '), code(to));
309 return d;
310 }));
311 append(tx, head, io); append(detail, tx);
312 });
225 document.getElementById('listview').style.display='none'; 313 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; 314 detail.style.display='block';
227 document.getElementById('home').style.display='inline'; 315 document.getElementById('home').style.display='inline';
228 window.scrollTo(0,0); 316 window.scrollTo(0,0);
229 }); 317 });
230} 318}
231document.getElementById('home').onclick=function(){ document.getElementById('detail').style.display='none'; document.getElementById('listview').style.display='block'; this.style.display='none'; }; 319document.getElementById('home').onclick=function(){
320 document.getElementById('detail').style.display='none';
321 document.getElementById('listview').style.display='block';
322 this.style.display='none';
323};
232document.getElementById('q').oninput=function(){ renderList(this.value); }; 324document.getElementById('q').oninput=function(){ renderList(this.value); };
233fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ 325fetch('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'; 326 SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks';
235 renderList(''); 327 renderList('');
236}); 328});
237</script> 329</script>
@@ -239,5 +331,6 @@ fetch('blocks.json').then(function(r){return r.json();}).then(function(d){
239</html> 331</html>
240""" 332"""
241 333
334
242if __name__ == "__main__": 335if __name__ == "__main__":
243 main() 336 raise SystemExit(main(sys.argv[1:]))