Initial SvelteKit block explorer for Triangles blockchain
Full-featured explorer consuming the daemon REST API: - Home dashboard with chain stats, latest blocks, staking info - Block list with pagination and block detail pages - Transaction detail with inputs/outputs tables - Address lookup with balance, UTXOs, and tx history - Staking dashboard with difficulty chart (Chart.js) - Network info with peer table - Mempool viewer - API documentation page for all 30+ REST endpoints - Search by block hash, height, txid, or address - Dark theme with Tailwind CSS v4 - SSR via adapter-node for SEO Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Error {page.status} - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col items-center justify-center py-20">
|
||||
<div class="text-6xl font-bold text-tri-accent mb-4">{page.status}</div>
|
||||
<div class="text-tri-text text-xl mb-2">
|
||||
{#if page.status === 404}
|
||||
Not Found
|
||||
{:else}
|
||||
Something went wrong
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-tri-muted text-sm mb-6">{page.error?.message || 'An unexpected error occurred'}</p>
|
||||
<a href="/" class="bg-tri-accent hover:bg-tri-accent-light text-white px-6 py-2 rounded-lg text-sm transition-colors">
|
||||
Back to Home
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getChainInfo, getNetwork } from '$lib/api';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
|
||||
export const load: LayoutServerLoad = async () => {
|
||||
try {
|
||||
const [chain, network] = await Promise.all([getChainInfo(), getNetwork()]);
|
||||
return { chain, network };
|
||||
} catch {
|
||||
return { chain: null, network: null };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||
import { formatNumber, formatAmount, formatDifficulty } from '$lib/utils';
|
||||
|
||||
let { data, children } = $props();
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: 'Home' },
|
||||
{ href: '/blocks', label: 'Blocks' },
|
||||
{ href: '/staking', label: 'Staking' },
|
||||
{ href: '/network', label: 'Network' },
|
||||
{ href: '/mempool', label: 'Mempool' },
|
||||
{ href: '/api', label: 'API' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Triangles Block Explorer</title>
|
||||
<meta name="description" content="Block explorer for the Triangles (TRI) cryptocurrency" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="min-h-screen bg-tri-bg text-tri-text flex flex-col">
|
||||
<!-- Header -->
|
||||
<header class="border-b border-tri-border bg-tri-surface/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto px-4 py-3">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<a href="/" class="text-xl font-bold text-white flex items-center gap-2 shrink-0">
|
||||
<span class="text-tri-accent">▲</span> Triangles Explorer
|
||||
</a>
|
||||
<SearchBar />
|
||||
</div>
|
||||
<nav class="flex gap-1 mt-2 overflow-x-auto">
|
||||
{#each navLinks as link}
|
||||
<a
|
||||
href={link.href}
|
||||
class="px-3 py-1.5 text-sm text-tri-muted hover:text-white hover:bg-tri-border/50 rounded transition-colors whitespace-nowrap"
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="flex-1 max-w-7xl mx-auto w-full px-4 py-6">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-tri-border bg-tri-surface/30 py-4">
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
{#if data.chain}
|
||||
<div class="flex flex-wrap gap-6 text-xs text-tri-muted justify-center">
|
||||
<span>Height: <strong class="text-tri-text">{formatNumber(data.chain.blocks)}</strong></span>
|
||||
<span>Supply: <strong class="text-tri-text">{formatAmount(data.chain.moneysupply)} TRI</strong></span>
|
||||
<span>PoS Diff: <strong class="text-tri-text">{formatDifficulty(data.chain.difficulty['proof-of-stake'])}</strong></span>
|
||||
{#if data.network}
|
||||
<span>Peers: <strong class="text-tri-text">{data.network.connections}</strong></span>
|
||||
{/if}
|
||||
<span>Chain: <strong class="text-tri-text">{data.chain.chain}</strong></span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-xs text-tri-muted">Daemon unavailable</div>
|
||||
{/if}
|
||||
<div class="text-center text-xs text-tri-muted mt-2">
|
||||
Triangles Block Explorer — <a href="https://cryptographic-triangles.org" class="text-tri-accent hover:underline">cryptographic-triangles.org</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
import { getChainInfo, getLatestBlocks, getStaking, getMining } from '$lib/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const [chain, blocks, staking, mining] = await Promise.all([
|
||||
getChainInfo(),
|
||||
getLatestBlocks(10),
|
||||
getStaking(),
|
||||
getMining()
|
||||
]);
|
||||
return { chain, blocks, staking, mining };
|
||||
} catch (e) {
|
||||
return { chain: null, blocks: [], staking: null, mining: null, error: String(e) };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import StatsCard from '$lib/components/StatsCard.svelte';
|
||||
import BlockTable from '$lib/components/BlockTable.svelte';
|
||||
import { formatNumber, formatAmount, formatDifficulty, truncateHash } from '$lib/utils';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Triangles Block Explorer - Dashboard</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if data.error}
|
||||
<div class="bg-tri-red/10 border border-tri-red/30 rounded-lg p-4 mb-6 text-tri-red text-sm">
|
||||
Unable to connect to daemon: {data.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.chain}
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatsCard label="Block Height" value={formatNumber(data.chain.blocks)} />
|
||||
<StatsCard
|
||||
label="Supply"
|
||||
value="{formatAmount(data.chain.moneysupply)} TRI"
|
||||
sub="of 120,000 max"
|
||||
/>
|
||||
<StatsCard
|
||||
label="PoS Difficulty"
|
||||
value={formatDifficulty(data.chain.difficulty['proof-of-stake'])}
|
||||
/>
|
||||
<StatsCard
|
||||
label="Best Block"
|
||||
value={truncateHash(data.chain.bestblockhash, 8)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Staking + Mining stats -->
|
||||
{#if data.staking || data.mining}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
{#if data.staking}
|
||||
<StatsCard
|
||||
label="Staking"
|
||||
value={data.staking.staking ? 'Active' : 'Inactive'}
|
||||
sub="Network weight: {formatNumber(data.staking.netstakeweight)}"
|
||||
/>
|
||||
{/if}
|
||||
{#if data.mining}
|
||||
<StatsCard
|
||||
label="Stake Interest"
|
||||
value="{data.mining.stakeinterest}%"
|
||||
sub="Annual rate"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Network Hash"
|
||||
value="{data.mining.netmhashps.toFixed(2)} MH/s"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Pooled Txs"
|
||||
value={formatNumber(data.mining.pooledtx)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Latest Blocks -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Latest Blocks</h2>
|
||||
<a href="/blocks" class="text-tri-accent text-sm hover:text-tri-accent-light">View all →</a>
|
||||
</div>
|
||||
{#if data.blocks.length > 0}
|
||||
<BlockTable blocks={data.blocks} />
|
||||
{:else}
|
||||
<div class="p-8 text-center text-tri-muted">No blocks available</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getAddressBalance, getAddressUtxos, getAddressTxids, validateAddress } from '$lib/api';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
try {
|
||||
const [validation, balance, utxos, txids] = await Promise.all([
|
||||
validateAddress(params.addr),
|
||||
getAddressBalance(params.addr),
|
||||
getAddressUtxos(params.addr),
|
||||
getAddressTxids(params.addr)
|
||||
]);
|
||||
|
||||
if (!validation.isvalid) {
|
||||
error(400, 'Invalid address');
|
||||
}
|
||||
|
||||
return { address: params.addr, balance, utxos, txids };
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
if (msg.includes('Address index not enabled')) {
|
||||
return { address: params.addr, balance: null, utxos: [], txids: [], indexError: true };
|
||||
}
|
||||
error(404, 'Address not found');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
<script lang="ts">
|
||||
import TxTable from '$lib/components/TxTable.svelte';
|
||||
import { formatAmount, truncateHash } from '$lib/utils';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Address {data.address} - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Address</h1>
|
||||
<p class="text-tri-accent text-sm font-mono mt-1 break-all">{data.address}</p>
|
||||
</div>
|
||||
|
||||
{#if data.indexError}
|
||||
<div class="bg-tri-yellow/10 border border-tri-yellow/30 rounded-lg p-4 text-tri-yellow text-sm">
|
||||
Address index is not enabled on this node. Start the daemon with <code class="bg-tri-surface px-1 rounded">-addressindex=1</code> to enable address lookups.
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Balance -->
|
||||
{#if data.balance}
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg p-6 mb-6">
|
||||
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">Balance</div>
|
||||
<div class="text-3xl font-bold text-white">
|
||||
{formatAmount(data.balance.balance / 100000000)} <span class="text-tri-muted text-lg">TRI</span>
|
||||
</div>
|
||||
<div class="text-tri-muted text-sm mt-1">
|
||||
Total received: {formatAmount(data.balance.received / 100000000)} TRI
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- UTXOs -->
|
||||
{#if data.utxos.length > 0}
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg mb-6 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Unspent Outputs ({data.utxos.length})</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-tri-muted text-xs uppercase tracking-wider border-b border-tri-border">
|
||||
<th class="text-left py-3 px-3">Txid</th>
|
||||
<th class="text-center py-3 px-3">Index</th>
|
||||
<th class="text-right py-3 px-3">Amount</th>
|
||||
<th class="text-right py-3 px-3">Height</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.utxos as utxo}
|
||||
<tr class="border-b border-tri-border/50">
|
||||
<td class="py-2.5 px-3 font-mono text-xs">
|
||||
<a href="/tx/{utxo.txid}" class="text-tri-accent hover:text-tri-accent-light">{truncateHash(utxo.txid, 10)}</a>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 text-center">{utxo.outputIndex}</td>
|
||||
<td class="py-2.5 px-3 text-right font-mono text-tri-green">{formatAmount(utxo.satoshis / 100000000)} TRI</td>
|
||||
<td class="py-2.5 px-3 text-right">{utxo.height}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Transaction History -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Transactions ({data.txids.length})</h2>
|
||||
</div>
|
||||
{#if data.txids.length > 0}
|
||||
<TxTable txids={data.txids} limit={50} />
|
||||
{#if data.txids.length > 50}
|
||||
<div class="px-4 py-3 text-center text-tri-muted text-sm border-t border-tri-border">
|
||||
Showing 50 of {data.txids.length} transactions
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="p-8 text-center text-tri-muted">No transactions found</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
const endpoints = [
|
||||
{ method: 'GET', path: '/rest/chaininfo', desc: 'Chain state: height, best block hash, supply, difficulty' },
|
||||
{ method: 'GET', path: '/rest/block/{hash}', desc: 'Block details by hash. Append .hex for raw hex.' },
|
||||
{ method: 'GET', path: '/rest/blockheader/{hash}', desc: 'Block header (lightweight, no tx list)' },
|
||||
{ method: 'GET', path: '/rest/blockbyheight/{n}', desc: 'Block details by height' },
|
||||
{ method: 'GET', path: '/rest/blockhashbyheight/{n}', desc: 'Get block hash at a given height' },
|
||||
{ method: 'GET', path: '/rest/tx/{txid}', desc: 'Transaction details. Append .hex for raw hex.' },
|
||||
{ method: 'GET', path: '/rest/mempool', desc: 'List of unconfirmed transaction IDs' },
|
||||
{ method: 'GET', path: '/rest/difficulty', desc: 'Current PoW and PoS difficulty' },
|
||||
{ method: 'GET', path: '/rest/supply', desc: 'UTXO set statistics and total supply' },
|
||||
{ method: 'GET', path: '/rest/staking', desc: 'Staking network stats (weight, difficulty, expected time)' },
|
||||
{ method: 'GET', path: '/rest/mining', desc: 'Mining info (hashrate, block value, stake interest)' },
|
||||
{ method: 'GET', path: '/rest/subsidy', desc: 'Current block subsidy/reward' },
|
||||
{ method: 'GET', path: '/rest/estimatefee', desc: 'Estimated fee per kilobyte' },
|
||||
{ method: 'GET', path: '/rest/checkpoint', desc: 'Sync checkpoint information' },
|
||||
{ method: 'GET', path: '/rest/network', desc: 'Network info (version, connections, proxy)' },
|
||||
{ method: 'GET', path: '/rest/peers', desc: 'List of connected peers with details' },
|
||||
{ method: 'GET', path: '/rest/validate/{addr}', desc: 'Validate a Triangles address' },
|
||||
{ method: 'GET', path: '/rest/address/{addr}/balance', desc: 'Address balance (requires -addressindex=1)' },
|
||||
{ method: 'GET', path: '/rest/address/{addr}/utxos', desc: 'Address unspent outputs (requires -addressindex=1)' },
|
||||
{ method: 'GET', path: '/rest/address/{addr}/txids', desc: 'Address transaction history. Supports ?start=N&end=N (requires -addressindex=1)' },
|
||||
{ method: 'POST', path: '/rest/tx/decode', desc: 'Decode a raw transaction. Body: {"hex":"..."}' },
|
||||
{ method: 'POST', path: '/rest/tx/send', desc: 'Broadcast a raw transaction. Body: {"hex":"..."}' },
|
||||
];
|
||||
|
||||
const walletEndpoints = [
|
||||
{ method: 'GET', path: '/rest/wallet/info', desc: 'Wallet summary (balance, tx count, keypool)' },
|
||||
{ method: 'GET', path: '/rest/wallet/balance', desc: 'Wallet balance' },
|
||||
{ method: 'GET', path: '/rest/wallet/transactions', desc: 'Recent wallet transactions. Supports ?count=N&skip=N' },
|
||||
{ method: 'GET', path: '/rest/wallet/transaction/{txid}', desc: 'Wallet transaction detail' },
|
||||
{ method: 'GET', path: '/rest/wallet/unspent', desc: 'Wallet unspent outputs. Supports ?minconf=N&maxconf=N' },
|
||||
{ method: 'GET', path: '/rest/wallet/addresses', desc: 'Wallet address groupings' },
|
||||
{ method: 'GET', path: '/rest/wallet/staking', desc: 'Staking info for this wallet' },
|
||||
{ method: 'POST', path: '/rest/wallet/address/new', desc: 'Generate a new receiving address' },
|
||||
{ method: 'POST', path: '/rest/wallet/send', desc: 'Send coins. Body: {"address":"...", "amount":N}' },
|
||||
{ method: 'POST', path: '/rest/wallet/sendmany', desc: 'Send to multiple addresses. Body: {"recipients":{"addr":amount}}' },
|
||||
{ method: 'POST', path: '/rest/wallet/unlock', desc: 'Unlock wallet. Body: {"passphrase":"...", "timeout":N}' },
|
||||
{ method: 'POST', path: '/rest/wallet/lock', desc: 'Lock the wallet' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>API Documentation - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-2">REST API Documentation</h1>
|
||||
<p class="text-tri-muted text-sm mb-8">
|
||||
The Triangles daemon exposes a REST API on the RPC port (default 19112) when started with <code class="bg-tri-surface px-1 rounded">-rest=1</code>.
|
||||
All responses are JSON. Public endpoints require no authentication.
|
||||
</p>
|
||||
|
||||
<!-- Public Endpoints -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg mb-8 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Public Endpoints</h2>
|
||||
<p class="text-tri-muted text-xs mt-1">No authentication required. Rate limited.</p>
|
||||
</div>
|
||||
<div class="divide-y divide-tri-border/50">
|
||||
{#each endpoints as ep}
|
||||
<div class="px-4 py-3 hover:bg-tri-bg/30 transition-colors">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="inline-block px-2 py-0.5 rounded text-xs font-bold {ep.method === 'GET' ? 'bg-tri-green/20 text-tri-green' : 'bg-tri-accent/20 text-tri-accent-light'}">
|
||||
{ep.method}
|
||||
</span>
|
||||
<code class="text-tri-text text-sm">{ep.path}</code>
|
||||
</div>
|
||||
<p class="text-tri-muted text-xs ml-14">{ep.desc}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wallet Endpoints -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Wallet Endpoints</h2>
|
||||
<p class="text-tri-muted text-xs mt-1">Require HTTP Basic Auth (RPC credentials) or Bearer token (<code>-restapikey</code>).</p>
|
||||
</div>
|
||||
<div class="divide-y divide-tri-border/50">
|
||||
{#each walletEndpoints as ep}
|
||||
<div class="px-4 py-3 hover:bg-tri-bg/30 transition-colors">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="inline-block px-2 py-0.5 rounded text-xs font-bold {ep.method === 'GET' ? 'bg-tri-green/20 text-tri-green' : 'bg-tri-accent/20 text-tri-accent-light'}">
|
||||
{ep.method}
|
||||
</span>
|
||||
<code class="text-tri-text text-sm">{ep.path}</code>
|
||||
</div>
|
||||
<p class="text-tri-muted text-xs ml-14">{ep.desc}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg mt-8 p-6">
|
||||
<h2 class="text-white font-semibold mb-4">Configuration</h2>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div>
|
||||
<code class="text-tri-accent">-rest=1</code>
|
||||
<span class="text-tri-muted ml-2">Enable the REST API</span>
|
||||
</div>
|
||||
<div>
|
||||
<code class="text-tri-accent">-addressindex=1</code>
|
||||
<span class="text-tri-muted ml-2">Enable address index (required for /address/* endpoints)</span>
|
||||
</div>
|
||||
<div>
|
||||
<code class="text-tri-accent">-restcorsorigin=*</code>
|
||||
<span class="text-tri-muted ml-2">Set CORS allowed origin (default: *)</span>
|
||||
</div>
|
||||
<div>
|
||||
<code class="text-tri-accent">-restapikey=<token></code>
|
||||
<span class="text-tri-muted ml-2">Bearer token for wallet endpoint authentication</span>
|
||||
</div>
|
||||
<div>
|
||||
<code class="text-tri-accent">-restratelimit=30</code>
|
||||
<span class="text-tri-muted ml-2">Rate limit per IP per second (default: 30, 0=disabled)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getBlock } from '$lib/api';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
try {
|
||||
const block = await getBlock(params.hash);
|
||||
return { block };
|
||||
} catch {
|
||||
error(404, 'Block not found');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import TxTable from '$lib/components/TxTable.svelte';
|
||||
import { formatTimestamp, formatDifficulty, blockType, truncateHash } from '$lib/utils';
|
||||
|
||||
let { data } = $props();
|
||||
const block = data.block;
|
||||
|
||||
const fields: [string, string][] = [
|
||||
['Height', String(block.height)],
|
||||
['Confirmations', String(block.confirmations)],
|
||||
['Timestamp', formatTimestamp(block.time)],
|
||||
['Type', blockType(block.flags)],
|
||||
['Difficulty', formatDifficulty(block.difficulty)],
|
||||
['Transactions', String(block.tx.length)],
|
||||
['Merkle Root', truncateHash(block.merkleroot, 16)],
|
||||
['Nonce', String(block.nonce)],
|
||||
['Bits', block.bits],
|
||||
['Size', block.size ? `${block.size} bytes` : 'N/A'],
|
||||
['Mint', String(block.mint)],
|
||||
['Flags', block.flags]
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Block {block.height} - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Block {block.height}</h1>
|
||||
<p class="text-tri-muted text-xs font-mono mt-1 break-all">{block.hash}</p>
|
||||
</div>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
{#if block.previousblockhash}
|
||||
<a href="/block/{block.previousblockhash}" class="bg-tri-surface border border-tri-border text-tri-text px-3 py-1.5 rounded text-sm hover:border-tri-accent transition-colors">← Prev</a>
|
||||
{/if}
|
||||
{#if block.nextblockhash}
|
||||
<a href="/block/{block.nextblockhash}" class="bg-tri-surface border border-tri-border text-tri-text px-3 py-1.5 rounded text-sm hover:border-tri-accent transition-colors">Next →</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Info -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg mb-6">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Block Details</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-px bg-tri-border">
|
||||
{#each fields as [label, value]}
|
||||
<div class="bg-tri-surface px-4 py-3">
|
||||
<span class="text-tri-muted text-xs uppercase tracking-wider">{label}</span>
|
||||
<div class="text-tri-text text-sm mt-0.5 font-mono break-all">{value}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transactions -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Transactions ({block.tx.length})</h2>
|
||||
</div>
|
||||
<TxTable txids={block.tx} />
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getChainInfo, getBlockByHeight } from '$lib/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { Block } from '$lib/types';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const page = parseInt(url.searchParams.get('page') || '1');
|
||||
|
||||
try {
|
||||
const chain = await getChainInfo();
|
||||
const startHeight = chain.blocks - (page - 1) * PAGE_SIZE;
|
||||
|
||||
const blocks: Block[] = [];
|
||||
for (let i = 0; i < PAGE_SIZE && startHeight - i >= 0; i++) {
|
||||
try {
|
||||
blocks.push(await getBlockByHeight(startHeight - i));
|
||||
} catch { break; }
|
||||
}
|
||||
|
||||
const hasNext = startHeight - PAGE_SIZE >= 0;
|
||||
return { blocks, page, hasNext, totalBlocks: chain.blocks };
|
||||
} catch (e) {
|
||||
return { blocks: [], page, hasNext: false, totalBlocks: 0, error: String(e) };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import BlockTable from '$lib/components/BlockTable.svelte';
|
||||
import Pagination from '$lib/components/Pagination.svelte';
|
||||
import { formatNumber } from '$lib/utils';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Blocks - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Blocks</h1>
|
||||
<p class="text-tri-muted text-sm mt-1">Total: {formatNumber(data.totalBlocks)} blocks</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
{#if data.blocks.length > 0}
|
||||
<BlockTable blocks={data.blocks} />
|
||||
{:else}
|
||||
<div class="p-8 text-center text-tri-muted">No blocks available</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Pagination page={data.page} hasNext={data.hasNext} basePath="/blocks" />
|
||||
@@ -0,0 +1,9 @@
|
||||
import { getBlockByHeight } from '$lib/api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const height = parseInt(params.height);
|
||||
const block = await getBlockByHeight(height);
|
||||
redirect(301, `/block/${block.hash}`);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<p class="text-tri-muted">Redirecting...</p>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getMempool } from '$lib/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const txids = await getMempool();
|
||||
return { txids };
|
||||
} catch (e) {
|
||||
return { txids: [], error: String(e) };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import TxTable from '$lib/components/TxTable.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Mempool - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Mempool</h1>
|
||||
<p class="text-tri-muted text-sm mt-1">{data.txids.length} unconfirmed transaction{data.txids.length !== 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
{#if data.txids.length > 0}
|
||||
<TxTable txids={data.txids} />
|
||||
{:else}
|
||||
<div class="p-8 text-center text-tri-muted">Mempool is empty</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { getNetwork, getPeers } from '$lib/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const [network, peers] = await Promise.all([getNetwork(), getPeers()]);
|
||||
return { network, peers };
|
||||
} catch (e) {
|
||||
return { network: null, peers: [], error: String(e) };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import StatsCard from '$lib/components/StatsCard.svelte';
|
||||
import PeerTable from '$lib/components/PeerTable.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Network - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-6">Network</h1>
|
||||
|
||||
{#if data.network}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatsCard label="Connections" value={String(data.network.connections)} />
|
||||
<StatsCard label="Protocol Version" value={String(data.network.protocolversion)} />
|
||||
<StatsCard label="Client Version" value={String(data.network.version)} />
|
||||
<StatsCard label="Network" value={data.network.testnet ? 'Testnet' : 'Mainnet'} />
|
||||
</div>
|
||||
|
||||
{#if data.network.errors}
|
||||
<div class="bg-tri-yellow/10 border border-tri-yellow/30 rounded-lg p-4 mb-6 text-tri-yellow text-sm">
|
||||
{data.network.errors}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Peers -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Connected Peers ({data.peers.length})</h2>
|
||||
</div>
|
||||
{#if data.peers.length > 0}
|
||||
<PeerTable peers={data.peers} />
|
||||
{:else}
|
||||
<div class="p-8 text-center text-tri-muted">No peers connected</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getBlock, getTransaction, getBlockHashByHeight } from '$lib/api';
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import { isNumeric, isBlockHash } from '$lib/utils';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const q = url.searchParams.get('q')?.trim();
|
||||
if (!q) error(400, 'No search query provided');
|
||||
|
||||
// Numeric: treat as block height
|
||||
if (isNumeric(q)) {
|
||||
try {
|
||||
const { blockhash } = await getBlockHashByHeight(parseInt(q));
|
||||
redirect(302, `/block/${blockhash}`);
|
||||
} catch {
|
||||
error(404, `Block at height ${q} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// 64-char hex: try block hash, then txid
|
||||
if (isBlockHash(q)) {
|
||||
try {
|
||||
await getBlock(q);
|
||||
redirect(302, `/block/${q}`);
|
||||
} catch {
|
||||
// Not a block, try as txid
|
||||
}
|
||||
try {
|
||||
await getTransaction(q);
|
||||
redirect(302, `/tx/${q}`);
|
||||
} catch {
|
||||
error(404, `No block or transaction found for ${q}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise: try as address
|
||||
redirect(302, `/address/${q}`);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<p class="text-tri-muted">Searching...</p>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getStaking, getMining, getSubsidy, getDifficulty, getChainInfo, getBlockHeader, getBlockHashByHeight } from '$lib/api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { BlockHeader } from '$lib/types';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
try {
|
||||
const [staking, mining, subsidy, difficulty, chain] = await Promise.all([
|
||||
getStaking(),
|
||||
getMining(),
|
||||
getSubsidy(),
|
||||
getDifficulty(),
|
||||
getChainInfo()
|
||||
]);
|
||||
|
||||
// Fetch last 50 block headers for difficulty chart
|
||||
const headers: BlockHeader[] = [];
|
||||
for (let i = 0; i < 50 && chain.blocks - i >= 0; i++) {
|
||||
try {
|
||||
const { blockhash } = await getBlockHashByHeight(chain.blocks - i);
|
||||
headers.push(await getBlockHeader(blockhash));
|
||||
} catch { break; }
|
||||
}
|
||||
|
||||
return { staking, mining, subsidy, difficulty, headers: headers.reverse() };
|
||||
} catch (e) {
|
||||
return { staking: null, mining: null, subsidy: null, difficulty: null, headers: [], error: String(e) };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import StatsCard from '$lib/components/StatsCard.svelte';
|
||||
import { formatNumber, formatDifficulty } from '$lib/utils';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
let { data } = $props();
|
||||
let chartCanvas: HTMLCanvasElement;
|
||||
|
||||
onMount(() => {
|
||||
if (data.headers.length > 0) {
|
||||
Chart.register(...registerables);
|
||||
new Chart(chartCanvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.headers.map(h => h.height),
|
||||
datasets: [{
|
||||
label: 'PoS Difficulty',
|
||||
data: data.headers.filter(h => h.flags.includes('proof-of-stake')).map(h => h.difficulty),
|
||||
borderColor: '#22c55e',
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { labels: { color: '#94a3b8' } } },
|
||||
scales: {
|
||||
x: { ticks: { color: '#94a3b8', maxTicksLimit: 10 }, grid: { color: '#2a2d3a' } },
|
||||
y: { ticks: { color: '#94a3b8' }, grid: { color: '#2a2d3a' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Staking - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-2xl font-bold text-white mb-6">Staking Dashboard</h1>
|
||||
|
||||
{#if data.staking && data.mining}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatsCard label="Staking Active" value={data.staking.staking ? 'Yes' : 'No'} />
|
||||
<StatsCard label="PoS Difficulty" value={formatDifficulty(data.staking.difficulty)} />
|
||||
<StatsCard label="Network Stake Weight" value={formatNumber(data.staking.netstakeweight)} />
|
||||
<StatsCard label="Stake Interest" value="{data.mining.stakeinterest}% annual" />
|
||||
<StatsCard label="Block Subsidy" value={data.subsidy != null ? String(data.subsidy) : 'N/A'} />
|
||||
<StatsCard label="Net Hash Rate" value="{data.mining.netmhashps.toFixed(2)} MH/s" />
|
||||
<StatsCard label="PoW Difficulty" value={data.difficulty ? formatDifficulty(data.difficulty['proof-of-work']) : 'N/A'} />
|
||||
<StatsCard
|
||||
label="Expected Time"
|
||||
value={data.staking.expectedtime > 0 ? `${Math.floor(data.staking.expectedtime / 3600)}h ${Math.floor((data.staking.expectedtime % 3600) / 60)}m` : 'N/A'}
|
||||
sub="Until next stake"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Difficulty Chart -->
|
||||
{#if data.headers.length > 0}
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
|
||||
<h2 class="text-white font-semibold mb-4">PoS Difficulty (Last 50 blocks)</h2>
|
||||
<canvas bind:this={chartCanvas}></canvas>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="bg-tri-red/10 border border-tri-red/30 rounded-lg p-4 text-tri-red text-sm">
|
||||
Unable to load staking data. Is the daemon running?
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getTransaction } from '$lib/api';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
try {
|
||||
const tx = await getTransaction(params.txid);
|
||||
return { tx };
|
||||
} catch {
|
||||
error(404, 'Transaction not found');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { formatTimestamp, formatAmount, truncateHash } from '$lib/utils';
|
||||
|
||||
let { data } = $props();
|
||||
const tx = data.tx;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Tx {truncateHash(tx.txid, 8)} - Triangles Explorer</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Transaction</h1>
|
||||
<p class="text-tri-muted text-xs font-mono mt-1 break-all">{tx.txid}</p>
|
||||
</div>
|
||||
|
||||
<!-- Tx Info -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg mb-6">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Details</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-px bg-tri-border">
|
||||
{#if tx.blockhash}
|
||||
<div class="bg-tri-surface px-4 py-3">
|
||||
<span class="text-tri-muted text-xs uppercase tracking-wider">Block</span>
|
||||
<div class="text-sm mt-0.5">
|
||||
<a href="/block/{tx.blockhash}" class="text-tri-accent hover:text-tri-accent-light font-mono">{truncateHash(tx.blockhash, 12)}</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="bg-tri-surface px-4 py-3">
|
||||
<span class="text-tri-muted text-xs uppercase tracking-wider">Confirmations</span>
|
||||
<div class="text-tri-text text-sm mt-0.5">{tx.confirmations ?? 'Unconfirmed'}</div>
|
||||
</div>
|
||||
<div class="bg-tri-surface px-4 py-3">
|
||||
<span class="text-tri-muted text-xs uppercase tracking-wider">Timestamp</span>
|
||||
<div class="text-tri-text text-sm mt-0.5 font-mono">{formatTimestamp(tx.time)}</div>
|
||||
</div>
|
||||
<div class="bg-tri-surface px-4 py-3">
|
||||
<span class="text-tri-muted text-xs uppercase tracking-wider">Version</span>
|
||||
<div class="text-tri-text text-sm mt-0.5">{tx.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inputs -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg mb-6 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Inputs ({tx.vin.length})</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-tri-muted text-xs uppercase tracking-wider border-b border-tri-border">
|
||||
<th class="text-left py-3 px-3">#</th>
|
||||
<th class="text-left py-3 px-3">Source</th>
|
||||
<th class="text-left py-3 px-3">Script</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tx.vin as vin, i}
|
||||
<tr class="border-b border-tri-border/50">
|
||||
<td class="py-2.5 px-3 text-tri-muted">{i}</td>
|
||||
<td class="py-2.5 px-3 font-mono text-xs">
|
||||
{#if vin.coinbase}
|
||||
<span class="text-tri-green">Coinbase / Staking reward</span>
|
||||
{:else if vin.txid}
|
||||
<a href="/tx/{vin.txid}" class="text-tri-accent hover:text-tri-accent-light">
|
||||
{truncateHash(vin.txid, 10)}:{vin.vout}
|
||||
</a>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="py-2.5 px-3 font-mono text-xs text-tri-muted max-w-xs truncate">
|
||||
{vin.scriptSig?.asm || vin.coinbase || ''}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Outputs -->
|
||||
<div class="bg-tri-surface border border-tri-border rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-tri-border">
|
||||
<h2 class="text-white font-semibold">Outputs ({tx.vout.length})</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-tri-muted text-xs uppercase tracking-wider border-b border-tri-border">
|
||||
<th class="text-left py-3 px-3">#</th>
|
||||
<th class="text-left py-3 px-3">Address</th>
|
||||
<th class="text-right py-3 px-3">Amount</th>
|
||||
<th class="text-left py-3 px-3">Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each tx.vout as vout}
|
||||
<tr class="border-b border-tri-border/50">
|
||||
<td class="py-2.5 px-3 text-tri-muted">{vout.n}</td>
|
||||
<td class="py-2.5 px-3 font-mono text-xs">
|
||||
{#if vout.scriptPubKey.addresses}
|
||||
{#each vout.scriptPubKey.addresses as addr}
|
||||
<a href="/address/{addr}" class="text-tri-accent hover:text-tri-accent-light">{addr}</a>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-tri-muted">N/A</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="py-2.5 px-3 text-right font-mono text-tri-green">{formatAmount(vout.value)} TRI</td>
|
||||
<td class="py-2.5 px-3 text-xs text-tri-muted">{vout.scriptPubKey.type}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user