mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-04-28 03:00:18 -04:00
explorer: Add /blocks endpoint with pagination
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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/<int:block_height>")
|
||||
async def get_block_by_height(block_height: int):
|
||||
if block_height < 0:
|
||||
|
||||
@@ -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;
|
||||
|
||||
88
bin/explorer/python/templates/blocks.html
Normal file
88
bin/explorer/python/templates/blocks.html
Normal file
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>All Blocks - DarkFi Block Explorer</title>
|
||||
<link rel="stylesheet" href="/static/layout.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="container flex justify-between items-center">
|
||||
<div class="logo">DarkFi {{ network }} Blocks</div>
|
||||
<nav class="nav">
|
||||
<a href="/">Explorer</a>
|
||||
<a href="/contracts">Contracts</a>
|
||||
<a href="/stats">Stats</a>
|
||||
<a href="https://dark.fi/book/">Docs</a>
|
||||
<a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container section">
|
||||
|
||||
<div class="table-section">
|
||||
<div class="table-header">
|
||||
<h3>All Blocks</h3>
|
||||
<span class="badge">{{ total }} Total</span>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<th>Size</th>
|
||||
<th>Txs</th>
|
||||
<th>Timestamp</th>
|
||||
<th>PoW</th>
|
||||
<th>Block Hash</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for block in blocks %}
|
||||
<tr>
|
||||
<td class="height"><a href="/block/{{ block.height|int }}">{{ block.height|int }}</a></td>
|
||||
<td>{{ block.size|int }}</td>
|
||||
<td>{{ block.n_txs|int }}</td>
|
||||
<td>{{ block.timestamp }}</td>
|
||||
<td>{{ block.powtype }}</td>
|
||||
<td><a href="/block/{{ block.height|int }}">{{ block.hash }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
{% if has_prev %}
|
||||
<a href="/blocks?page=1" class="btn btn-sm">« First</a>
|
||||
<a href="/blocks?page={{ page - 1 }}" class="btn btn-sm">‹ Prev</a>
|
||||
{% else %}
|
||||
<span class="btn btn-sm disabled">« First</span>
|
||||
<span class="btn btn-sm disabled">‹ Prev</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="pagination-info">Page {{ page }} of {{ total_pages }}</span>
|
||||
|
||||
{% if has_next %}
|
||||
<a href="/blocks?page={{ page + 1 }}" class="btn btn-sm">Next ›</a>
|
||||
<a href="/blocks?page={{ total_pages }}" class="btn btn-sm">Last »</a>
|
||||
{% else %}
|
||||
<span class="btn btn-sm disabled">Next ›</span>
|
||||
<span class="btn btn-sm disabled">Last »</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<a href="https://dark.fi">Let there be Dark</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -79,7 +79,7 @@
|
||||
<div class="table-section">
|
||||
<div class="table-header">
|
||||
<h3>Latest Blocks</h3>
|
||||
<a href="#">View all →</a>
|
||||
<a href="/blocks">View all →</a>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
|
||||
Reference in New Issue
Block a user