Add lightweight blockchain explorer

This commit is contained in:
juvdiaz 2026-06-29 19:16:13 -06:00
parent 945b2a09a7
commit 3738f81416
3 changed files with 207 additions and 0 deletions

View File

@ -21,6 +21,12 @@ The default RPC endpoint is:
http://127.0.0.1:8545
```
The lightweight local explorer is available at:
```text
http://127.0.0.1:8089
```
From another LAN host, use the Debian host IP:
```text
@ -43,6 +49,7 @@ curl -sS -X POST http://127.0.0.1:8545 \
```bash
BLOCKCHAIN_RPC_PORT=8545
BLOCKCHAIN_EXPLORER_PORT=8089
BLOCKCHAIN_CHAIN_ID=31337
BLOCKCHAIN_BLOCK_TIME=2
```

View File

@ -12,6 +12,19 @@ services:
- "${BLOCKCHAIN_CHAIN_ID:-31337}"
- --block-time
- "${BLOCKCHAIN_BLOCK_TIME:-2}"
- --allow-origin
- "*"
ports:
- "${BLOCKCHAIN_RPC_PORT:-8545}:8545"
restart: unless-stopped
explorer:
image: nginx:1.27-alpine
container_name: homelab-blockchain-explorer
depends_on:
- anvil
ports:
- "${BLOCKCHAIN_EXPLORER_PORT:-8089}:80"
volumes:
- ./explorer:/usr/share/nginx/html:ro
restart: unless-stopped

View File

@ -0,0 +1,187 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Homelab Devnet Explorer</title>
<style>
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.4;
}
body {
margin: 0;
background: #111827;
color: #f9fafb;
}
main {
width: min(980px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0;
}
h1 {
margin: 0 0 8px;
font-size: 1.8rem;
}
.toolbar,
.grid,
pre {
border: 1px solid #374151;
background: #1f2937;
border-radius: 8px;
}
.toolbar {
display: flex;
gap: 8px;
align-items: center;
padding: 12px;
margin: 24px 0;
}
input {
flex: 1;
min-width: 0;
padding: 10px;
border: 1px solid #4b5563;
border-radius: 6px;
background: #111827;
color: #f9fafb;
}
button {
padding: 10px 14px;
border: 0;
border-radius: 6px;
background: #38bdf8;
color: #0f172a;
font-weight: 700;
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1px;
overflow: hidden;
}
.metric {
padding: 16px;
background: #111827;
}
.metric span {
display: block;
color: #9ca3af;
font-size: 0.78rem;
text-transform: uppercase;
}
.metric strong {
display: block;
margin-top: 6px;
font-size: 1.2rem;
word-break: break-word;
}
pre {
padding: 16px;
white-space: pre-wrap;
word-break: break-word;
min-height: 120px;
}
</style>
</head>
<body>
<main>
<h1>Homelab Devnet Explorer</h1>
<p>Local Anvil chain view. Use only dev wallets and dev keys.</p>
<section class="toolbar">
<input id="rpcUrl" value="http://127.0.0.1:8545" aria-label="JSON-RPC URL">
<button id="refresh">Refresh</button>
</section>
<section class="grid" aria-label="Devnet metrics">
<div class="metric">
<span>Chain ID</span>
<strong id="chainId">unknown</strong>
</div>
<div class="metric">
<span>Latest Block</span>
<strong id="blockNumber">unknown</strong>
</div>
<div class="metric">
<span>Gas Price</span>
<strong id="gasPrice">unknown</strong>
</div>
<div class="metric">
<span>Peer Count</span>
<strong id="peerCount">unknown</strong>
</div>
</section>
<h2>Latest Block</h2>
<pre id="latestBlock">Waiting for refresh...</pre>
</main>
<script>
const fields = {
chainId: document.querySelector("#chainId"),
blockNumber: document.querySelector("#blockNumber"),
gasPrice: document.querySelector("#gasPrice"),
peerCount: document.querySelector("#peerCount"),
latestBlock: document.querySelector("#latestBlock"),
rpcUrl: document.querySelector("#rpcUrl")
};
async function rpc(method, params = []) {
const response = await fetch(fields.rpcUrl.value, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: Date.now() })
});
const payload = await response.json();
if (payload.error) {
throw new Error(payload.error.message || JSON.stringify(payload.error));
}
return payload.result;
}
function hexToDecimal(value) {
return Number.parseInt(value, 16).toString();
}
async function refresh() {
fields.latestBlock.textContent = "Loading...";
try {
const [chainId, blockNumber, gasPrice, peerCount] = await Promise.all([
rpc("eth_chainId"),
rpc("eth_blockNumber"),
rpc("eth_gasPrice"),
rpc("net_peerCount")
]);
fields.chainId.textContent = hexToDecimal(chainId);
fields.blockNumber.textContent = hexToDecimal(blockNumber);
fields.gasPrice.textContent = `${hexToDecimal(gasPrice)} wei`;
fields.peerCount.textContent = hexToDecimal(peerCount);
const block = await rpc("eth_getBlockByNumber", [blockNumber, false]);
fields.latestBlock.textContent = JSON.stringify(block, null, 2);
} catch (error) {
fields.latestBlock.textContent = error.message;
}
}
document.querySelector("#refresh").addEventListener("click", refresh);
refresh();
setInterval(refresh, 10000);
</script>
</body>
</html>