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:
2026-03-15 21:04:36 -07:00
commit b6a4bd0418
43 changed files with 3995 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
TRIANGLES_API_URL=http://127.0.0.1:19112
+6
View File
@@ -0,0 +1,6 @@
node_modules
.svelte-kit
build
.env
.env.*
!.env.example
+2495
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "triangles-explorer",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/kit": "^2.21.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.0.0",
"svelte": "^5.20.0",
"svelte-check": "^4.1.0",
"tailwindcss": "^4.0.0",
"tslib": "^2.8.1",
"typescript": "^5.7.0",
"vite": "^6.2.0"
},
"dependencies": {
"chart.js": "^4.4.0"
}
}
+14
View File
@@ -0,0 +1,14 @@
@import 'tailwindcss';
@theme {
--color-tri-bg: #0f1117;
--color-tri-surface: #1a1d27;
--color-tri-border: #2a2d3a;
--color-tri-accent: #6366f1;
--color-tri-accent-light: #818cf8;
--color-tri-green: #22c55e;
--color-tri-red: #ef4444;
--color-tri-yellow: #eab308;
--color-tri-text: #e2e8f0;
--color-tri-muted: #94a3b8;
}
+13
View File
@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+70
View File
@@ -0,0 +1,70 @@
import { env } from '$env/dynamic/private';
import type {
ChainInfo, Block, BlockHeader, Transaction, Difficulty,
StakingInfo, MiningInfo, NetworkInfo, PeerInfo,
AddressBalance, AddressUtxo, SupplyInfo, ValidationResult
} from './types';
const BASE_URL = env.TRIANGLES_API_URL || 'http://127.0.0.1:19112';
async function get<T>(path: string): Promise<T> {
const res = await fetch(`${BASE_URL}/rest/${path}`);
if (!res.ok) {
const body = await res.text();
throw new Error(`API error ${res.status}: ${body}`);
}
return res.json();
}
// Chain info
export const getChainInfo = () => get<ChainInfo>('chaininfo');
// Blocks
export const getBlock = (hash: string) => get<Block>(`block/${hash}`);
export const getBlockHeader = (hash: string) => get<BlockHeader>(`blockheader/${hash}`);
export const getBlockByHeight = (n: number) => get<Block>(`blockbyheight/${n}`);
export const getBlockHashByHeight = (n: number) => get<{ blockhash: string }>(`blockhashbyheight/${n}`);
// Transactions
export const getTransaction = (txid: string) => get<Transaction>(`tx/${txid}`);
// Mempool
export const getMempool = () => get<string[]>('mempool');
// Network
export const getDifficulty = () => get<Difficulty>('difficulty');
export const getSupply = () => get<SupplyInfo>('supply');
export const getStaking = () => get<StakingInfo>('staking');
export const getMining = () => get<MiningInfo>('mining');
export const getSubsidy = () => get<number>('subsidy');
export const getEstimateFee = () => get<number>('estimatefee');
export const getCheckpoint = () => get<Record<string, unknown>>('checkpoint');
export const getNetwork = () => get<NetworkInfo>('network');
export const getPeers = () => get<PeerInfo[]>('peers');
// Address (requires -addressindex=1)
export const getAddressBalance = (addr: string) => get<AddressBalance>(`address/${addr}/balance`);
export const getAddressUtxos = (addr: string) => get<AddressUtxo[]>(`address/${addr}/utxos`);
export const getAddressTxids = (addr: string, start?: number, end?: number) => {
let path = `address/${addr}/txids`;
const params: string[] = [];
if (start !== undefined) params.push(`start=${start}`);
if (end !== undefined) params.push(`end=${end}`);
if (params.length) path += '?' + params.join('&');
return get<string[]>(path);
};
// Validation
export const validateAddress = (addr: string) => get<ValidationResult>(`validate/${addr}`);
// Helper: get latest N blocks
export async function getLatestBlocks(count: number): Promise<Block[]> {
const chain = await getChainInfo();
const blocks: Block[] = [];
for (let i = 0; i < count && chain.blocks - i >= 0; i++) {
try {
blocks.push(await getBlockByHeight(chain.blocks - i));
} catch { break; }
}
return blocks;
}
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts">
import type { Block } from '$lib/types';
import { truncateHash, timeAgo, blockType, formatDifficulty } from '$lib/utils';
let { blocks }: { blocks: Block[] } = $props();
</script>
<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">Height</th>
<th class="text-left py-3 px-3">Hash</th>
<th class="text-left py-3 px-3">Age</th>
<th class="text-center py-3 px-3">Txs</th>
<th class="text-center py-3 px-3">Type</th>
<th class="text-right py-3 px-3">Difficulty</th>
</tr>
</thead>
<tbody>
{#each blocks as block}
<tr class="border-b border-tri-border/50 hover:bg-tri-surface/50 transition-colors">
<td class="py-2.5 px-3">
<a href="/block/{block.hash}" class="text-tri-accent hover:text-tri-accent-light">
{block.height}
</a>
</td>
<td class="py-2.5 px-3 font-mono text-xs">
<a href="/block/{block.hash}" class="text-tri-text hover:text-tri-accent-light">
{truncateHash(block.hash, 10)}
</a>
</td>
<td class="py-2.5 px-3 text-tri-muted">{timeAgo(block.time)}</td>
<td class="py-2.5 px-3 text-center">{block.tx.length}</td>
<td class="py-2.5 px-3 text-center">
<span class="inline-block px-2 py-0.5 rounded text-xs font-medium {block.flags.includes('proof-of-stake') ? 'bg-tri-green/20 text-tri-green' : 'bg-tri-yellow/20 text-tri-yellow'}">
{blockType(block.flags)}
</span>
</td>
<td class="py-2.5 px-3 text-right font-mono text-xs">{formatDifficulty(block.difficulty)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts">
let { page, hasNext, basePath }: { page: number; hasNext: boolean; basePath: string } = $props();
</script>
<div class="flex items-center justify-between mt-6">
{#if page > 1}
<a
href="{basePath}?page={page - 1}"
class="bg-tri-surface border border-tri-border text-tri-text px-4 py-2 rounded-lg text-sm hover:border-tri-accent transition-colors"
>
Previous
</a>
{:else}
<div></div>
{/if}
<span class="text-tri-muted text-sm">Page {page}</span>
{#if hasNext}
<a
href="{basePath}?page={page + 1}"
class="bg-tri-surface border border-tri-border text-tri-text px-4 py-2 rounded-lg text-sm hover:border-tri-accent transition-colors"
>
Next
</a>
{:else}
<div></div>
{/if}
</div>
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
import type { PeerInfo } from '$lib/types';
import { timeAgo } from '$lib/utils';
let { peers }: { peers: PeerInfo[] } = $props();
</script>
<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">Address</th>
<th class="text-left py-3 px-3">Version</th>
<th class="text-center py-3 px-3">Direction</th>
<th class="text-right py-3 px-3">Height</th>
<th class="text-right py-3 px-3">Connected</th>
</tr>
</thead>
<tbody>
{#each peers as peer}
<tr class="border-b border-tri-border/50 hover:bg-tri-surface/50 transition-colors">
<td class="py-2.5 px-3 font-mono text-xs">{peer.addr}</td>
<td class="py-2.5 px-3 text-xs text-tri-muted">{peer.subver}</td>
<td class="py-2.5 px-3 text-center">
<span class="inline-block px-2 py-0.5 rounded text-xs font-medium {peer.inbound ? 'bg-tri-green/20 text-tri-green' : 'bg-tri-accent/20 text-tri-accent-light'}">
{peer.inbound ? 'In' : 'Out'}
</span>
</td>
<td class="py-2.5 px-3 text-right">{peer.startingheight}</td>
<td class="py-2.5 px-3 text-right text-tri-muted">{timeAgo(peer.conntime)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { isBlockHash, isNumeric, isAddress } from '$lib/utils';
let query = $state('');
function handleSearch() {
const q = query.trim();
if (!q) return;
if (isNumeric(q)) {
goto(`/blocks/${q}`);
} else if (isBlockHash(q)) {
goto(`/search?q=${q}`);
} else if (isAddress(q)) {
goto(`/address/${q}`);
} else {
goto(`/search?q=${q}`);
}
query = '';
}
</script>
<form onsubmit={handleSearch} class="flex w-full max-w-xl">
<input
type="text"
bind:value={query}
placeholder="Search block hash, height, txid, or address..."
class="flex-1 bg-tri-surface border border-tri-border rounded-l-lg px-4 py-2 text-sm text-tri-text placeholder-tri-muted focus:outline-none focus:border-tri-accent"
/>
<button
type="submit"
class="bg-tri-accent hover:bg-tri-accent-light text-white px-4 py-2 rounded-r-lg text-sm font-medium transition-colors"
>
Search
</button>
</form>
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
let { label, value, sub = '' }: { label: string; value: string; sub?: string } = $props();
</script>
<div class="bg-tri-surface border border-tri-border rounded-lg p-4">
<div class="text-tri-muted text-xs uppercase tracking-wider mb-1">{label}</div>
<div class="text-tri-text text-lg font-semibold">{value}</div>
{#if sub}
<div class="text-tri-muted text-xs mt-1">{sub}</div>
{/if}
</div>
+29
View File
@@ -0,0 +1,29 @@
<script lang="ts">
import { truncateHash } from '$lib/utils';
let { txids, limit = 0 }: { txids: string[]; limit?: number } = $props();
const displayed = limit > 0 ? txids.slice(0, limit) : txids;
</script>
<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">Transaction ID</th>
</tr>
</thead>
<tbody>
{#each displayed as txid, i}
<tr class="border-b border-tri-border/50 hover:bg-tri-surface/50 transition-colors">
<td class="py-2.5 px-3 text-tri-muted">{i + 1}</td>
<td class="py-2.5 px-3 font-mono text-xs">
<a href="/tx/{txid}" class="text-tri-accent hover:text-tri-accent-light">
{truncateHash(txid, 16)}
</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
+165
View File
@@ -0,0 +1,165 @@
export interface ChainInfo {
chain: string;
blocks: number;
bestblockhash: string;
difficulty: {
'proof-of-work': number;
'proof-of-stake': number;
};
moneysupply: number;
}
export interface Block {
hash: string;
confirmations: number;
size: number;
height: number;
version: number;
merkleroot: string;
mint: number;
time: number;
nonce: number;
bits: string;
difficulty: number;
blocktrust: string;
chaintrust: string;
previousblockhash?: string;
nextblockhash?: string;
flags: string;
proofhash: string;
entropybit: number;
modifier: string;
tx: string[];
}
export interface BlockHeader {
hash: string;
confirmations: number;
height: number;
version: number;
merkleroot: string;
time: number;
nonce: number;
bits: string;
difficulty: number;
flags: string;
previousblockhash?: string;
nextblockhash?: string;
}
export interface Transaction {
txid: string;
version: number;
time: number;
locktime: number;
vin: TxInput[];
vout: TxOutput[];
blockhash?: string;
confirmations?: number;
blocktime?: number;
}
export interface TxInput {
txid?: string;
vout?: number;
scriptSig?: {
asm: string;
hex: string;
};
coinbase?: string;
sequence: number;
}
export interface TxOutput {
value: number;
n: number;
scriptPubKey: {
asm: string;
hex: string;
reqSigs?: number;
type: string;
addresses?: string[];
};
}
export interface Difficulty {
'proof-of-work': number;
'proof-of-stake': number;
'search-interval': number;
}
export interface StakingInfo {
enabled: boolean;
staking: boolean;
errors: string;
currentblocksize: number;
currentblocktx: number;
difficulty: number;
'search-interval': number;
weight: number;
netstakeweight: number;
expectedtime: number;
}
export interface MiningInfo {
blocks: number;
currentblocksize: number;
currentblocktx: number;
difficulty: number;
blockvalue: number;
netmhashps: number;
netstakeweight: number;
errors: string;
pooledtx: number;
stakedifficulty: number;
stakeinterest: number;
testnet: boolean;
}
export interface NetworkInfo {
version: number;
protocolversion: number;
connections: number;
proxy: string;
testnet: boolean;
errors: string;
}
export interface PeerInfo {
addr: string;
services: string;
lastsend: number;
lastrecv: number;
conntime: number;
version: number;
subver: string;
inbound: boolean;
startingheight: number;
banscore: number;
}
export interface AddressBalance {
balance: number;
received: number;
}
export interface AddressUtxo {
address: string;
txid: string;
outputIndex: number;
satoshis: number;
height: number;
}
export interface SupplyInfo {
height: number;
bestblock: string;
total_amount: number;
}
export interface ValidationResult {
isvalid: boolean;
address?: string;
ismine?: boolean;
account?: string;
}
+52
View File
@@ -0,0 +1,52 @@
export function timeAgo(timestamp: number): string {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
export function truncateHash(hash: string, chars = 8): string {
if (hash.length <= chars * 2 + 3) return hash;
return `${hash.slice(0, chars)}...${hash.slice(-chars)}`;
}
export function formatAmount(amount: number): string {
return amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 });
}
export function formatDifficulty(diff: number): string {
if (diff >= 1e12) return (diff / 1e12).toFixed(2) + 'T';
if (diff >= 1e9) return (diff / 1e9).toFixed(2) + 'G';
if (diff >= 1e6) return (diff / 1e6).toFixed(2) + 'M';
if (diff >= 1e3) return (diff / 1e3).toFixed(2) + 'K';
return diff.toFixed(4);
}
export function formatNumber(n: number): string {
return n.toLocaleString();
}
export function formatTimestamp(ts: number): string {
return new Date(ts * 1000).toLocaleString();
}
export function isBlockHash(input: string): boolean {
return /^[0-9a-fA-F]{64}$/.test(input);
}
export function isNumeric(input: string): boolean {
return /^\d+$/.test(input);
}
export function isAddress(input: string): boolean {
return /^[A-Za-z1-9]{25,34}$/.test(input);
}
export function blockType(flags: string): string {
if (flags.includes('proof-of-stake')) return 'PoS';
return 'PoW';
}
+22
View File
@@ -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>
+11
View File
@@ -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 };
}
};
+72
View File
@@ -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">&#9650;</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 &mdash; <a href="https://cryptographic-triangles.org" class="text-tri-accent hover:underline">cryptographic-triangles.org</a>
</div>
</div>
</footer>
</div>
+16
View File
@@ -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) };
}
};
+78
View File
@@ -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 &rarr;</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>
+26
View File
@@ -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');
}
};
+84
View File
@@ -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}
+120
View File
@@ -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=&lt;token&gt;</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>
+12
View File
@@ -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');
}
};
+65
View File
@@ -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">&larr; 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 &rarr;</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>
+26
View File
@@ -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) };
}
};
+26
View File
@@ -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}`);
};
+1
View File
@@ -0,0 +1 @@
<p class="text-tri-muted">Redirecting...</p>
+11
View File
@@ -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) };
}
};
+22
View File
@@ -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>
+11
View File
@@ -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) };
}
};
+39
View File
@@ -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>
+38
View File
@@ -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}`);
};
+1
View File
@@ -0,0 +1 @@
<p class="text-tri-muted">Searching...</p>
+28
View File
@@ -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) };
}
};
+73
View File
@@ -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}
+12
View File
@@ -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');
}
};
+118
View File
@@ -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>
+12
View File
@@ -0,0 +1,12 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter()
}
};
export default config;
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}
+7
View File
@@ -0,0 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
});