diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /scripts/build-explorer.py | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff) | |
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we
build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped
line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git
merge-base returns no result at all -- so anyone cloning this repository
landed on a default branch that was neither current nor related to the
running node.
The 28 commits on main were audited before this merge: every one is already
present in the tree by content, including the two that are hardest to see
because they were rewritten afterwards -- the payout reorg-safety work
(PayoutBlockStillOnChain, since evolved to re-check on every retry rather
than once at maturity) and the nostr relay certificate verification
(SSL_set1_host, now with a Windows root-store path). Nothing on main is lost
by this merge.
Recorded as a merge rather than a force-push: main only ever moves forward,
its old history stays reachable as the first parent, and this is revertible.
The tree is taken wholesale from the merged branch, so main's content is now
exactly what was built and tested.
What this brings onto main:
- compactblock: BIP152-style compact block relay ported from upstream 1.2.23,
primitives and unit test only, with the RAND_bytes failure path closed and
the reconstruction allocation bounded.
- upstream/main 17741d9: isolated testnet parameters and genesis, optional
2 MB large pages for RandomX. Mainnet genesis verified byte-identical
through upstream's BuildGenesisBlock() refactor.
- deploy: every image built on bookworm so binaries run on Debian 12, and the
container build now honours the RandomX and libsecp256k1 pins it had been
silently ignoring since before 1.2.15.
Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus
constants unchanged against e4f898c apart from the pre-existing
MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'scripts/build-explorer.py')
| -rwxr-xr-x | scripts/build-explorer.py | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py new file mode 100755 index 0000000..dbb8e17 --- /dev/null +++ b/scripts/build-explorer.py | |||
| @@ -0,0 +1,376 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Build a static Bitflash block explorer from local blk*.dat files.""" | ||
| 3 | |||
| 4 | import argparse | ||
| 5 | import hashlib | ||
| 6 | import json | ||
| 7 | import shutil | ||
| 8 | import sys | ||
| 9 | from pathlib import Path | ||
| 10 | |||
| 11 | import bitflash_chain as chain | ||
| 12 | |||
| 13 | |||
| 14 | ADDRESS_VERSION = 25 | ||
| 15 | B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | ||
| 16 | |||
| 17 | |||
| 18 | def b58check(payload): | ||
| 19 | v = bytes([ADDRESS_VERSION]) + payload | ||
| 20 | v += chain.sha256d(v)[:4] | ||
| 21 | n = int.from_bytes(v, "big") | ||
| 22 | s = "" | ||
| 23 | while n > 0: | ||
| 24 | n, r = divmod(n, 58) | ||
| 25 | s = B58[r] + s | ||
| 26 | for byte in v: | ||
| 27 | if byte == 0: | ||
| 28 | s = "1" + s | ||
| 29 | else: | ||
| 30 | break | ||
| 31 | return s | ||
| 32 | |||
| 33 | |||
| 34 | def hash160(b): | ||
| 35 | return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest() | ||
| 36 | |||
| 37 | |||
| 38 | def spk_info(script_hex): | ||
| 39 | try: | ||
| 40 | spk = bytes.fromhex(script_hex) | ||
| 41 | except ValueError: | ||
| 42 | return {"type": "malformed", "hex": script_hex[:80]} | ||
| 43 | if ( | ||
| 44 | len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9 | ||
| 45 | and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC | ||
| 46 | ): | ||
| 47 | return {"type": "p2pkh", "address": b58check(spk[3:23])} | ||
| 48 | if len(spk) >= 35 and spk[-1] == 0xAC: | ||
| 49 | if spk[0] == 0x41 and len(spk) == 67: | ||
| 50 | pk = spk[1:66] | ||
| 51 | return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} | ||
| 52 | if spk[0] == 0x21 and len(spk) == 35: | ||
| 53 | pk = spk[1:34] | ||
| 54 | return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} | ||
| 55 | return {"type": "other", "hex": script_hex[:80]} | ||
| 56 | |||
| 57 | |||
| 58 | def explorer_tx(tx): | ||
| 59 | vin = [] | ||
| 60 | for txin in tx["vin"]: | ||
| 61 | coinbase = ( | ||
| 62 | txin["prevout_hash"] == "0" * 64 | ||
| 63 | and txin["prevout_n"] == 0xFFFFFFFF | ||
| 64 | ) | ||
| 65 | vin.append({ | ||
| 66 | "coinbase": coinbase, | ||
| 67 | "prev": None if coinbase else txin["prevout_hash"], | ||
| 68 | "vout": None if coinbase else txin["prevout_n"], | ||
| 69 | }) | ||
| 70 | vout = [] | ||
| 71 | for out in tx["vout"]: | ||
| 72 | vout.append(dict({"value": out["value_satoshis"]}, **spk_info(out["script_pub_key_hex"]))) | ||
| 73 | return {"txid": tx["txid"], "vin": vin, "vout": vout, "size": len(chain.ser_tx(tx))} | ||
| 74 | |||
| 75 | |||
| 76 | def parse_args(argv): | ||
| 77 | ap = argparse.ArgumentParser(description=__doc__) | ||
| 78 | ap.add_argument("--datadir", help="Bitflash data directory containing blk*.dat") | ||
| 79 | ap.add_argument("--max-blocks", type=int, default=0, | ||
| 80 | help="number of blocks to scan; 0 means all block files") | ||
| 81 | ap.add_argument("paths", nargs="+", | ||
| 82 | help="with --datadir: <out-dir>; otherwise: <blk*.dat>... <out-dir>") | ||
| 83 | args = ap.parse_args(argv) | ||
| 84 | if args.datadir: | ||
| 85 | if len(args.paths) != 1: | ||
| 86 | raise SystemExit("usage with --datadir: build-explorer.py --datadir DATADIR <out-dir>") | ||
| 87 | args.block_files = None | ||
| 88 | args.out_dir = args.paths[0] | ||
| 89 | else: | ||
| 90 | if len(args.paths) < 2: | ||
| 91 | raise SystemExit("usage: build-explorer.py <blk*.dat>... <out-dir>") | ||
| 92 | args.block_files = args.paths[:-1] | ||
| 93 | args.out_dir = args.paths[-1] | ||
| 94 | return args | ||
| 95 | |||
| 96 | |||
| 97 | def load_blocks(args): | ||
| 98 | if args.datadir: | ||
| 99 | return chain.read_blocks(args.datadir, args.max_blocks) | ||
| 100 | return chain.read_blocks_from_files(args.block_files, args.max_blocks) | ||
| 101 | |||
| 102 | |||
| 103 | def build_explorer(raw_blocks, out_dir): | ||
| 104 | try: | ||
| 105 | blocks = chain.select_main_chain(raw_blocks) | ||
| 106 | except chain.ParseError as e: | ||
| 107 | raise SystemExit(str(e)) | ||
| 108 | out = Path(out_dir) | ||
| 109 | (out / "block").mkdir(parents=True, exist_ok=True) | ||
| 110 | |||
| 111 | summaries = [] | ||
| 112 | for height, block in enumerate(blocks): | ||
| 113 | txs = [explorer_tx(tx) for tx in block["transactions"]] | ||
| 114 | detail = { | ||
| 115 | "height": height, | ||
| 116 | "hash": block["hash"], | ||
| 117 | "prev": block["previous_hash"], | ||
| 118 | "merkleRoot": block["merkle_root"], | ||
| 119 | "version": block["version"], | ||
| 120 | "time": block["time"], | ||
| 121 | "bits": int(block["bits"], 16), | ||
| 122 | "nonce": block["nonce"], | ||
| 123 | "size": block["size"], | ||
| 124 | "txs": txs, | ||
| 125 | } | ||
| 126 | cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {} | ||
| 127 | summaries.append({ | ||
| 128 | "height": height, | ||
| 129 | "hash": block["hash"], | ||
| 130 | "time": block["time"], | ||
| 131 | "nTx": len(txs), | ||
| 132 | "size": block["size"], | ||
| 133 | "cbValue": cb.get("value", 0), | ||
| 134 | "cbAddr": cb.get("address", ""), | ||
| 135 | }) | ||
| 136 | (out / "block" / ("%d.json" % height)).write_text( | ||
| 137 | json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n", | ||
| 138 | encoding="ascii", | ||
| 139 | ) | ||
| 140 | |||
| 141 | summaries.sort(key=lambda x: -x["height"]) | ||
| 142 | (out / "blocks.json").write_text( | ||
| 143 | json.dumps({ | ||
| 144 | "network": "bitflash", | ||
| 145 | "tipHeight": len(blocks) - 1, | ||
| 146 | "count": len(blocks), | ||
| 147 | "blocks": summaries, | ||
| 148 | }, sort_keys=True, separators=(",", ":")) + "\n", | ||
| 149 | encoding="ascii", | ||
| 150 | ) | ||
| 151 | (out / "index.html").write_text(INDEX_HTML, encoding="utf-8") | ||
| 152 | (out / "style.css").write_text(STYLE_CSS, encoding="utf-8") | ||
| 153 | (out / "explorer.js").write_text(EXPLORER_JS, encoding="utf-8") | ||
| 154 | logo = Path(__file__).resolve().parents[1] / "docs" / "logo.png" | ||
| 155 | if logo.exists(): | ||
| 156 | shutil.copyfile(logo, out / "logo.png") | ||
| 157 | return len(blocks), len(blocks) - 1 | ||
| 158 | |||
| 159 | |||
| 160 | def main(argv): | ||
| 161 | args = parse_args(argv) | ||
| 162 | raw_blocks = load_blocks(args) | ||
| 163 | if not raw_blocks: | ||
| 164 | raise SystemExit("no Bitflash blocks parsed") | ||
| 165 | count, tip = build_explorer(raw_blocks, args.out_dir) | ||
| 166 | print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir)) | ||
| 167 | return 0 | ||
| 168 | |||
| 169 | |||
| 170 | INDEX_HTML = r"""<!doctype html> | ||
| 171 | <html lang="en"> | ||
| 172 | <head> | ||
| 173 | <meta charset="utf-8"> | ||
| 174 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 175 | <title>Bitflash Block Explorer</title> | ||
| 176 | <meta name="description" content="Read-only Bitflash block explorer generated from local blk*.dat files."> | ||
| 177 | <link rel="icon" href="logo.png" type="image/png"> | ||
| 178 | <link rel="stylesheet" href="style.css"> | ||
| 179 | <script src="explorer.js" defer></script> | ||
| 180 | </head> | ||
| 181 | <body> | ||
| 182 | <header class="topbar"> | ||
| 183 | <a class="brand" href="https://bitflash.network/"><img src="logo.png" width="22" height="22" alt="">Bitflash</a> | ||
| 184 | <nav aria-label="Bitflash sites"> | ||
| 185 | <a href="https://bitflash.network/">home</a> | ||
| 186 | <a href="https://git.bitflash.network/bitflash/bitflash">code</a> | ||
| 187 | <a href="https://docs.bitflash.network/">docs</a> | ||
| 188 | <a href="https://releases.bitflash.network/">downloads</a> | ||
| 189 | <a href="https://explorer.bitflash.network/" aria-current="page">explorer</a> | ||
| 190 | <a href="https://status.bitflash.network/">status</a> | ||
| 191 | </nav> | ||
| 192 | <span class="meta" id="tip"></span> | ||
| 193 | </header> | ||
| 194 | <div class="wrap"> | ||
| 195 | <h1>Block explorer <button class="link" id="home" hidden>back to blocks</button></h1> | ||
| 196 | <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> | ||
| 197 | <div id="listview"> | ||
| 198 | <input id="q" placeholder="Search by height or block hash"> | ||
| 199 | <div class="scroll"><table> | ||
| 200 | <thead><tr><th>Height</th><th>Time (UTC)</th><th>Txs</th><th>Size</th><th>Coinbase</th><th>To</th></tr></thead> | ||
| 201 | <tbody id="rows"></tbody> | ||
| 202 | </table></div> | ||
| 203 | <p class="mut" id="more"></p> | ||
| 204 | </div> | ||
| 205 | <div id="detail" hidden></div> | ||
| 206 | </div> | ||
| 207 | </body> | ||
| 208 | </html> | ||
| 209 | """ | ||
| 210 | |||
| 211 | |||
| 212 | STYLE_CSS = r""":root{ | ||
| 213 | color-scheme: light dark; | ||
| 214 | --bg:#fff; --bg-2:#f6f6f6; --fg:#111; --dim:#6b6b6b; --line:#d7d7d7; --line-hard:#111; | ||
| 215 | --accent:#0b7a34; --accent-2:#12a047; --bar:3rem; | ||
| 216 | --mono: ui-monospace,"Cascadia Mono","Segoe UI Mono",Consolas,"DejaVu Sans Mono","Liberation Mono","Courier New",monospace; | ||
| 217 | } | ||
| 218 | @media (prefers-color-scheme: dark){:root{ | ||
| 219 | --bg:#000; --bg-2:#0b0b0b; --fg:#dcdcdc; --dim:#8a8a8a; --line:#242424; --line-hard:#4a4a4a; | ||
| 220 | --accent:#2ee06a; --accent-2:#6bff9f; | ||
| 221 | }} | ||
| 222 | *{box-sizing:border-box;} | ||
| 223 | html,body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 var(--mono);-webkit-text-size-adjust:100%;} | ||
| 224 | a{color:var(--accent);} | ||
| 225 | .topbar{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;min-height:var(--bar); | ||
| 226 | padding:.5rem clamp(.9rem,2.5vw,3rem);border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:5;} | ||
| 227 | .brand{display:flex;align-items:center;gap:.6rem;text-decoration:none;color:var(--fg);font-weight:bold;} | ||
| 228 | .brand img{width:22px;height:22px;display:block;} | ||
| 229 | .topbar nav{display:flex;flex-wrap:wrap;gap:.15rem 1.4rem;font-size:.86rem;} | ||
| 230 | .topbar nav a{text-decoration:none;color:var(--dim);} | ||
| 231 | .topbar nav a::before{content:"/";color:var(--line-hard);margin-right:.3rem;} | ||
| 232 | .topbar nav a:hover{color:var(--fg);text-decoration:underline;} | ||
| 233 | .topbar nav a[aria-current="page"]{color:var(--accent);font-weight:bold;} | ||
| 234 | .topbar nav a[aria-current="page"]::before{color:var(--accent);} | ||
| 235 | .topbar .meta{margin-left:auto;font-size:.76rem;color:var(--dim);letter-spacing:.06em;} | ||
| 236 | .wrap{padding:1.4rem clamp(.9rem,2.5vw,3rem) 4rem;} | ||
| 237 | h1{font-size:1rem;margin:0 0 .3rem;letter-spacing:.02em;} | ||
| 238 | h2{font-size:.95rem;margin:1.2rem 0 .4rem;} | ||
| 239 | .mut{color:var(--dim);} | ||
| 240 | .note{color:var(--dim);font-size:.82rem;max-width:60rem;margin:.2rem 0 1rem;} | ||
| 241 | input{width:100%;max-width:28rem;padding:.5rem .7rem;border:1px solid var(--line);background:var(--bg-2);color:var(--fg);font:inherit;font-size:.86rem;} | ||
| 242 | input:focus{outline:none;border-color:var(--accent);} | ||
| 243 | .scroll{overflow-x:auto;} | ||
| 244 | table{width:100%;border-collapse:collapse;margin-top:.8rem;font-size:.86rem;} | ||
| 245 | th,td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid var(--line);font-variant-numeric:tabular-nums;} | ||
| 246 | th{font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--dim);font-weight:normal;} | ||
| 247 | tr.blk{cursor:pointer;} | ||
| 248 | tr.blk:hover td{background:var(--bg-2);} | ||
| 249 | code{font-size:.8rem;word-break:break-all;color:var(--fg);} | ||
| 250 | .card{border:1px solid var(--line);background:var(--bg-2);padding:.9rem 1rem;margin-top:.8rem;} | ||
| 251 | .kv{display:grid;grid-template-columns:11rem 1fr;gap:.25rem .8rem;font-size:.82rem;} | ||
| 252 | .kv div:nth-child(odd){color:var(--dim);} | ||
| 253 | .tx{border-top:1px solid var(--line);padding:.7rem 0;} | ||
| 254 | .io{display:grid;grid-template-columns:1fr 1fr;gap:1rem;} | ||
| 255 | @media (max-width:640px){.io{grid-template-columns:1fr;}.kv{grid-template-columns:7rem 1fr;}.topbar .meta{margin-left:0;width:100%;}} | ||
| 256 | .pill{display:inline-block;font-size:.68rem;padding:.05rem .5rem;border:1px solid var(--line);color:var(--dim);text-transform:uppercase;letter-spacing:.05em;} | ||
| 257 | button.link{background:none;border:none;color:var(--accent);cursor:pointer;font:inherit;padding:0;text-decoration:underline;} | ||
| 258 | """ | ||
| 259 | |||
| 260 | |||
| 261 | EXPLORER_JS = r""" | ||
| 262 | var SUM = [], SHOWN = 200; | ||
| 263 | function btf(sat){ return (sat/1e8).toFixed(8).replace(/0+$/,'').replace(/\.$/,'')+" BTF"; } | ||
| 264 | function ts(t){ return new Date(t*1000).toISOString().replace('T',' ').replace('.000Z',' UTC'); } | ||
| 265 | function short(h){ return h ? h.slice(0,10)+"..."+h.slice(-6) : "-"; } | ||
| 266 | function E(tag, cls, text){ | ||
| 267 | var e = document.createElement(tag); | ||
| 268 | if(cls) e.className = cls; | ||
| 269 | if(text !== undefined) e.textContent = text; | ||
| 270 | return e; | ||
| 271 | } | ||
| 272 | function code(text){ return E('code', '', text); } | ||
| 273 | function clear(e){ while(e.firstChild) e.removeChild(e.firstChild); } | ||
| 274 | function append(parent){ | ||
| 275 | for(var i=1;i<arguments.length;i++) parent.appendChild(arguments[i]); | ||
| 276 | return parent; | ||
| 277 | } | ||
| 278 | function row(cells, cls){ | ||
| 279 | var tr = E('tr', cls || ''); | ||
| 280 | cells.forEach(function(c){ append(tr, E('td', '', c)); }); | ||
| 281 | return tr; | ||
| 282 | } | ||
| 283 | function renderList(filter){ | ||
| 284 | var q=(filter||"").trim().toLowerCase(), rows=document.getElementById('rows'); | ||
| 285 | clear(rows); | ||
| 286 | var list = q ? SUM.filter(function(b){ return String(b.height)===q || b.hash.indexOf(q)===0; }) : SUM.slice(0,SHOWN); | ||
| 287 | if(!list.length){ | ||
| 288 | var empty = E('tr'); | ||
| 289 | var td = E('td', 'mut', 'no match'); | ||
| 290 | td.colSpan = 6; | ||
| 291 | append(empty, td); append(rows, empty); | ||
| 292 | } | ||
| 293 | list.forEach(function(b){ | ||
| 294 | var tr = row([String(b.height), ts(b.time), String(b.nTx), b.size+' B', btf(b.cbValue), ''], 'blk'); | ||
| 295 | tr.dataset.h = b.height; | ||
| 296 | clear(tr.children[5]); | ||
| 297 | append(tr.children[5], code(b.cbAddr || '-')); | ||
| 298 | tr.onclick = function(){ openBlock(tr.dataset.h); }; | ||
| 299 | append(rows, tr); | ||
| 300 | }); | ||
| 301 | document.getElementById('more').textContent = (!q && SUM.length>SHOWN) ? ("showing latest "+SHOWN+" of "+SUM.length+" blocks; search to find any") : ""; | ||
| 302 | } | ||
| 303 | function kv(parent, key, value, isCode){ | ||
| 304 | append(parent, E('div', '', key)); | ||
| 305 | append(parent, isCode ? append(E('div'), code(value)) : E('div', '', value)); | ||
| 306 | } | ||
| 307 | function ioSide(title, items, render){ | ||
| 308 | var side = E('div'); | ||
| 309 | append(side, E('div', 'mut', title)); | ||
| 310 | items.forEach(function(item){ append(side, render(item)); }); | ||
| 311 | return side; | ||
| 312 | } | ||
| 313 | function openBlock(h){ | ||
| 314 | fetch('block/'+h+'.json').then(function(r){return r.json();}).then(function(b){ | ||
| 315 | var detail = document.getElementById('detail'); | ||
| 316 | clear(detail); | ||
| 317 | append(detail, E('h2', '', 'Block '+b.height)); | ||
| 318 | var card = E('div', 'card'), grid = E('div', 'kv'); | ||
| 319 | kv(grid, 'Height', String(b.height)); | ||
| 320 | kv(grid, 'Hash', b.hash, true); | ||
| 321 | var prev = E('div'); | ||
| 322 | if(b.height > 0){ | ||
| 323 | var btn = E('button', 'link'); | ||
| 324 | append(btn, code(short(b.prev))); | ||
| 325 | btn.onclick = function(){ openBlock(b.height - 1); }; | ||
| 326 | append(prev, btn); | ||
| 327 | } else { | ||
| 328 | append(prev, code(short(b.prev))); | ||
| 329 | } | ||
| 330 | append(grid, E('div', '', 'Previous'), prev); | ||
| 331 | kv(grid, 'Merkle root', b.merkleRoot, true); | ||
| 332 | kv(grid, 'Time (UTC)', ts(b.time)); | ||
| 333 | kv(grid, 'Bits / Nonce', b.bits.toString(16)+' / '+b.nonce); | ||
| 334 | kv(grid, 'Size / Txs', b.size+' B / '+b.txs.length); | ||
| 335 | append(card, grid); append(detail, card); | ||
| 336 | b.txs.forEach(function(t, i){ | ||
| 337 | var tx = E('div', 'tx'); | ||
| 338 | var head = E('div'); | ||
| 339 | append(head, code(t.txid)); | ||
| 340 | if(i === 0) append(head, E('span', 'pill', 'coinbase')); | ||
| 341 | var io = E('div', 'io'); | ||
| 342 | append(io, ioSide('Inputs', t.vin, function(v){ | ||
| 343 | var d = E('div'); | ||
| 344 | if(v.coinbase) append(d, E('span', 'pill', 'coinbase')); | ||
| 345 | else append(d, code(short(v.prev)+':'+v.vout)); | ||
| 346 | return d; | ||
| 347 | })); | ||
| 348 | append(io, ioSide('Outputs', t.vout, function(o){ | ||
| 349 | var d = E('div'); | ||
| 350 | var to = o.address ? o.address : (o.pubkey ? 'pubkey '+o.pubkey.slice(0,16)+'...' : o.type); | ||
| 351 | append(d, document.createTextNode(btf(o.value)+' -> '), code(to)); | ||
| 352 | return d; | ||
| 353 | })); | ||
| 354 | append(tx, head, io); append(detail, tx); | ||
| 355 | }); | ||
| 356 | document.getElementById('listview').hidden = true; | ||
| 357 | detail.hidden = false; | ||
| 358 | document.getElementById('home').hidden = false; | ||
| 359 | window.scrollTo(0,0); | ||
| 360 | }); | ||
| 361 | } | ||
| 362 | document.getElementById('home').onclick=function(){ | ||
| 363 | document.getElementById('detail').hidden = true; | ||
| 364 | document.getElementById('listview').hidden = false; | ||
| 365 | this.hidden = true; | ||
| 366 | }; | ||
| 367 | document.getElementById('q').oninput=function(){ renderList(this.value); }; | ||
| 368 | fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ | ||
| 369 | SUM=d.blocks; document.getElementById('tip').textContent='tip height '+d.tipHeight+' - '+d.count+' blocks'; | ||
| 370 | renderList(''); | ||
| 371 | }); | ||
| 372 | """ | ||
| 373 | |||
| 374 | |||
| 375 | if __name__ == "__main__": | ||
| 376 | raise SystemExit(main(sys.argv[1:])) | ||