diff options
| author | bitflash <admin@bitflash.network> | 2026-08-12 10:31:29 -0300 |
|---|---|---|
| committer | bitflash <admin@bitflash.network> | 2026-08-12 10:31:29 -0300 |
| commit | c8d798807e69ddbe55bfab8a34286ab75b786cb5 (patch) | |
| tree | 81b5d44e5193f220b64097f496e314e04caf2db8 /scripts/build-explorer.py | |
| parent | c5f6b52c0d1d65f7bf8cd745a7a212bc6b2fe318 (diff) | |
| parent | d0ebe648f333def99fababd65f6b929448616d56 (diff) | |
test merge
# Conflicts:
# src/makefile.mingw
Diffstat (limited to 'scripts/build-explorer.py')
| -rwxr-xr-x | scripts/build-explorer.py | 385 |
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 | |||
| 16 | import sys, os, json, hashlib, struct | ||
| 17 | |||
| 18 | MAGIC = bytes([0xbf, 0x20, 0x5c, 0xfd]) | ||
| 19 | ADDRESS_VERSION = 25 # Bitflash addresses start with "B" | ||
| 20 | |||
| 21 | def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest() | ||
| 22 | 3 | ||
| 4 | import argparse | ||
| 5 | import hashlib | ||
| 6 | import json | ||
| 7 | import sys | ||
| 8 | from pathlib import Path | ||
| 9 | |||
| 10 | import bitflash_chain as chain | ||
| 11 | |||
| 12 | |||
| 13 | ADDRESS_VERSION = 25 | ||
| 23 | B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | 14 | B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" |
| 15 | |||
| 16 | |||
| 24 | def b58check(payload): | 17 | def 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 | |||
| 36 | def hash160(b): | 33 | def hash160(b): |
| 37 | return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest() | 34 | return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest() |
| 38 | 35 | ||
| 39 | class R: | 36 | |
| 40 | def __init__(s, b, o=0): s.b = b; s.o = o | 37 | def 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 | |||
| 53 | def 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]} | |
| 65 | def parse_tx(r): | 55 | |
| 66 | start = r.o | 56 | |
| 67 | r.i32() | 57 | def 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 = [] | |
| 80 | def 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) | 75 | def 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): | 96 | def 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] | 102 | def 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 | |||
| 154 | def 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 | ||
| 134 | INDEX_HTML = r"""<!doctype html> | 164 | INDEX_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> |
| 189 | var SUM = [], BYH = {}, SHOWN = 200; | 219 | var SUM = [], SHOWN = 200; |
| 190 | function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } | 220 | function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } |
| 191 | function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); } | 221 | function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); } |
| 192 | function esc(s){ return String(s).replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c];}); } | 222 | function short(h){ return h ? h.slice(0,10)+"..."+h.slice(-6) : "-"; } |
| 193 | function short(h){ return h ? h.slice(0,10)+"…"+h.slice(-6) : "-"; } | 223 | function 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 | } | ||
| 229 | function code(text){ return E('code', '', text); } | ||
| 230 | function clear(e){ while(e.firstChild) e.removeChild(e.firstChild); } | ||
| 231 | function append(parent){ | ||
| 232 | for(var i=1;i<arguments.length;i++) parent.appendChild(arguments[i]); | ||
| 233 | return parent; | ||
| 234 | } | ||
| 235 | function row(cells, cls){ | ||
| 236 | var tr = E('tr', cls || ''); | ||
| 237 | cells.forEach(function(c){ append(tr, E('td', '', c)); }); | ||
| 238 | return tr; | ||
| 239 | } | ||
| 195 | function renderList(filter){ | 240 | function 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 | } |
| 205 | function ioSide(title, items, html){ | 260 | function 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 | } | ||
| 264 | function 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 | } |
| 208 | function openBlock(h){ | 270 | function 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 | } |
| 231 | document.getElementById('home').onclick=function(){ document.getElementById('detail').style.display='none'; document.getElementById('listview').style.display='block'; this.style.display='none'; }; | 319 | document.getElementById('home').onclick=function(){ |
| 320 | document.getElementById('detail').style.display='none'; | ||
| 321 | document.getElementById('listview').style.display='block'; | ||
| 322 | this.style.display='none'; | ||
| 323 | }; | ||
| 232 | document.getElementById('q').oninput=function(){ renderList(this.value); }; | 324 | document.getElementById('q').oninput=function(){ renderList(this.value); }; |
| 233 | fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ | 325 | fetch('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 | |||
| 242 | if __name__ == "__main__": | 335 | if __name__ == "__main__": |
| 243 | main() | 336 | raise SystemExit(main(sys.argv[1:])) |