From df0763da2d74bf1aa6776ecd187fce6574e249a5 Mon Sep 17 00:00:00 2001 From: x Date: Tue, 27 Jan 2026 14:17:35 +0000 Subject: [PATCH] explorer: Add /blocks endpoint with pagination --- Cargo.lock | 5 +- bin/explorer/python/explorer.py | 65 +++++++++++++++++ bin/explorer/python/static/layout.css | 56 +++++++++++++++ bin/explorer/python/templates/blocks.html | 88 +++++++++++++++++++++++ bin/explorer/python/templates/index.html | 2 +- 5 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 bin/explorer/python/templates/blocks.html diff --git a/Cargo.lock b/Cargo.lock index ba6be93c1..81d7c1c4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2308,7 +2308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337f65eb93d9996551b9442423480eca4532586b337484446eb5138d0cd8fcf0" dependencies = [ "heck 0.5.0", - "indexmap 2.13.0", + "indexmap 1.9.3", "itertools 0.14.0", "proc-macro-crate", "proc-macro2", @@ -6945,12 +6945,11 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tapes" version = "0.1.0" -source = "git+https://github.com/Cuprate/Tapes#a30a5261564b12b21b126599149a88d5a0c5087c" +source = "git+https://github.com/Cuprate/Tapes#5bb3fc964341f0d608c13275e3496d43d4b28ab1" dependencies = [ "blake2", "borsh", "bytemuck", - "libc", "parking_lot 0.12.5", "slab", ] diff --git a/bin/explorer/python/explorer.py b/bin/explorer/python/explorer.py index 52923cc20..58a07616d 100755 --- a/bin/explorer/python/explorer.py +++ b/bin/explorer/python/explorer.py @@ -15,6 +15,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import asyncio from datetime import datetime, timezone from quart import Quart, render_template, abort, request, redirect, url_for, Response @@ -150,6 +151,70 @@ async def index(): ) +@app.route("/blocks") +async def list_blocks(): + """List all blocks with pagination""" + BLOCKS_PER_PAGE = 100 + + # Get page number from query param (1-indexed for users) + page = request.args.get("page", 1, type=int) + if page < 1: + page = 1 + + current_difficulty = await rpc.call("current_difficulty", params=[]) + current_height = await rpc.call("current_height", params=[]) + hashrate = await rpc.call("get_hashrate", params=[]) + + total = current_height + 1 # blocks 0 to current_height + + # Calculate range: newest first + offset = (page - 1) * BLOCKS_PER_PAGE + start_height = current_height - offset + end_height = max(0, start_height - BLOCKS_PER_PAGE + 1) + + # Build list of heights to fetch + heights = [h for h in range(start_height, end_height - 1, -1) if h >= 0] + + async def fetch_block(height): + try: + block = await rpc.call("get_block", params=[height]) + dt = datetime.fromtimestamp(block["timestamp"], tz=timezone.utc) + return { + "height": int(block["height"]), + "size": int(block["size"]), + "n_txs": len(block["txs"]), + "timestamp": dt.strftime("%H:%M UTC %d %b %Y"), + "powtype": block["powtype"], + "hash": block["hash"], + } + except JsonRpcError: + return None + + # Fetch all blocks in parallel + results = await asyncio.gather(*[fetch_block(h) for h in heights]) + blocks = [b for b in results if b is not None] + + # Calculate pagination info + total_pages = (total + BLOCKS_PER_PAGE - 1) // BLOCKS_PER_PAGE + has_prev = page > 1 + has_next = page < total_pages + + return await render_template( + "blocks.html", + network=app.config["NETWORK"], + current_difficulty=current_difficulty[0], + current_height=current_height, + hashrate=format_hashrate(hashrate), + blocks=blocks, + total=total, + page=page, + total_pages=total_pages, + has_prev=has_prev, + has_next=has_next, + blocks_per_page=BLOCKS_PER_PAGE, + ) + + @app.route("/block/") async def get_block_by_height(block_height: int): if block_height < 0: diff --git a/bin/explorer/python/static/layout.css b/bin/explorer/python/static/layout.css index d44421893..94d029f00 100644 --- a/bin/explorer/python/static/layout.css +++ b/bin/explorer/python/static/layout.css @@ -335,6 +335,10 @@ code { overflow: hidden; } +.table-section.full-width { + width: 100%; +} + .table-header { display: flex; justify-content: space-between; @@ -562,6 +566,58 @@ tbody tr:hover td { background: var(--color-hover); } white-space: pre-wrap; } +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + padding: 1.5rem; + border-top: 1px solid var(--table-border-color); +} + +.pagination .btn-sm { + padding: 0.5rem 1rem; + font-size: 1.3rem; +} + +.pagination .btn-sm.disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.pagination-info { + font-size: 1.4rem; + color: var(--color-muted); + padding: 0 1rem; +} + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + padding: 1.5rem; + border-top: 1px solid var(--table-border-color); +} + +.pagination .btn-sm { + padding: 0.5rem 1rem; + font-size: 1.3rem; +} + +.pagination .btn-sm.disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.pagination-info { + font-size: 1.4rem; + color: var(--color-muted); + padding: 0 1rem; +} + .footer { border-top: 1px solid var(--table-border-color); padding: 3rem 0; diff --git a/bin/explorer/python/templates/blocks.html b/bin/explorer/python/templates/blocks.html new file mode 100644 index 000000000..37927be53 --- /dev/null +++ b/bin/explorer/python/templates/blocks.html @@ -0,0 +1,88 @@ + + + + + + All Blocks - DarkFi Block Explorer + + + +
+
+ + +
+
+ +
+ +
+
+

All Blocks

+ {{ total }} Total +
+ +
+ + + + + + + + + + + + + {% for block in blocks %} + + + + + + + + + {% endfor %} + +
HeightSizeTxsTimestampPoWBlock Hash
{{ block.height|int }}{{ block.size|int }}{{ block.n_txs|int }}{{ block.timestamp }}{{ block.powtype }}{{ block.hash }}
+
+ + +
+ +
+ + + + + diff --git a/bin/explorer/python/templates/index.html b/bin/explorer/python/templates/index.html index 59a9cd6ee..90d4ce77c 100644 --- a/bin/explorer/python/templates/index.html +++ b/bin/explorer/python/templates/index.html @@ -79,7 +79,7 @@

Latest Blocks

- View all → + View all →