diff --git a/package-lock.json b/package-lock.json index 73c1971..a708f9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1013,7 +1013,6 @@ "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -1056,7 +1055,6 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -1411,7 +1409,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2157,7 +2154,6 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -2261,7 +2257,6 @@ "integrity": "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2379,7 +2374,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2394,7 +2388,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/src/app.css b/src/app.css index 9c0cf0d..e544c7c 100644 --- a/src/app.css +++ b/src/app.css @@ -1,14 +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-bg: #000000; + --color-tri-surface: #1a0000; + --color-tri-border: #330000; + --color-tri-accent: #dc2626; + --color-tri-accent-light: #ef4444; --color-tri-green: #22c55e; --color-tri-red: #ef4444; --color-tri-yellow: #eab308; - --color-tri-text: #e2e8f0; - --color-tri-muted: #94a3b8; + --color-tri-text: #ffffff; + --color-tri-muted: #999999; } diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..a98fec3 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,11 @@ +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = async ({ event, resolve }) => { + const start = Date.now(); + const response = await resolve(event); + const duration = Date.now() - start; + + console.log(`[${response.status}] ${event.request.method} ${event.url.pathname} (${duration}ms)`); + + return response; +}; diff --git a/src/lib/api.ts b/src/lib/api.ts index b9140ce..d449e73 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -8,16 +8,54 @@ import type { const BASE_URL = env.TRIANGLES_API_URL || 'http://127.0.0.1:19112'; async function get(path: string): Promise { - const res = await fetch(`${BASE_URL}/rest/${path}`); + const t0 = Date.now(); + const url = `${BASE_URL}/rest/${path}`; + const res = await fetch(url); + const fetchTime = Date.now() - t0; if (!res.ok) { const body = await res.text(); throw new Error(`API error ${res.status}: ${body}`); } - return res.json(); + const data = await res.json(); + console.log(`[API] GET ${path} took ${Date.now() - t0}ms (fetch: ${fetchTime}ms)`); + return data; } -// Chain info -export const getChainInfo = () => get('chaininfo'); +// In-memory cache with deduplication for expensive endpoints +const apiCache = new Map }>(); +const API_CACHE_TTL = 10000; // 10 seconds + +async function getCached(path: string, ttl = API_CACHE_TTL): Promise { + const now = Date.now(); + const entry = apiCache.get(path); + + // Return cached data if fresh + if (entry?.data && (now - entry.timestamp) < ttl) { + console.log(`[API] CACHE HIT ${path}`); + return entry.data as T; + } + + // Deduplicate in-flight requests + if (entry?.promise) { + console.log(`[API] DEDUP ${path}`); + return entry.promise as Promise; + } + + // Make the request + const promise = get(path).then(data => { + apiCache.set(path, { data, timestamp: Date.now() }); + return data; + }).catch(err => { + apiCache.delete(path); + throw err; + }); + + apiCache.set(path, { data: entry?.data ?? null, timestamp: entry?.timestamp ?? 0, promise }); + return promise; +} + +// Chain info (cached + deduplicated — expensive due to gettxoutsetinfo) +export const getChainInfo = () => getCached('chaininfo'); // Blocks export const getBlock = (hash: string) => get(`block/${hash}`); @@ -57,14 +95,16 @@ export const getAddressTxids = (addr: string, start?: number, end?: number) => { // Validation export const validateAddress = (addr: string) => get(`validate/${addr}`); -// Helper: get latest N blocks +// Helper: get latest N blocks (parallel fetch for performance) export async function getLatestBlocks(count: number): Promise { 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; + const heights = Array.from({ length: count }, (_, i) => chain.blocks - i).filter(h => h >= 0); + + // Fetch all blocks in parallel + const results = await Promise.allSettled(heights.map(h => getBlockByHeight(h))); + + // Return only successful fetches + return results + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map(r => r.value); } diff --git a/src/lib/components/BlockTable.svelte b/src/lib/components/BlockTable.svelte index b8d0be9..9ff1c28 100644 --- a/src/lib/components/BlockTable.svelte +++ b/src/lib/components/BlockTable.svelte @@ -2,7 +2,11 @@ import type { Block } from '$lib/types'; import { truncateHash, timeAgo, blockType, formatDifficulty } from '$lib/utils'; - let { blocks }: { blocks: Block[] } = $props(); + interface BlockWithAddresses extends Block { + addresses?: string[]; + } + + let { blocks }: { blocks: BlockWithAddresses[] } = $props();
@@ -13,6 +17,7 @@ Hash Age Txs + Addresses Type Difficulty @@ -21,7 +26,7 @@ {#each blocks as block} - + {block.height} @@ -32,6 +37,26 @@ {timeAgo(block.time)} {block.tx.length} + + {#if block.addresses && block.addresses.length > 0} +
+ {#each block.addresses.slice(0, 3) as addr} + + {truncateHash(addr, 8)} + + {/each} + {#if block.addresses.length > 3} + +{block.addresses.length - 3} + {/if} +
+ {:else} + + {/if} + {blockType(block.flags)} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 11b184c..d2108a1 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,16 +1,34 @@ -import { getChainInfo, getLatestBlocks, getStaking, getMining } from '$lib/api'; +import { getChainInfo, getBlockByHeight, getStaking, getMining } from '$lib/api'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async () => { + const t0 = Date.now(); try { - const [chain, blocks, staking, mining] = await Promise.all([ - getChainInfo(), - getLatestBlocks(10), + console.log('[SSR] Starting homepage load...'); + + // Fetch chaininfo first to get current height + const t1 = Date.now(); + const chain = await getChainInfo(); + console.log(`[SSR] getChainInfo took ${Date.now() - t1}ms`); + + // Then fetch everything else in parallel + const heights = Array.from({ length: 10 }, (_, i) => chain.blocks - i).filter(h => h >= 0); + const t2 = Date.now(); + const [blocks, staking, mining] = await Promise.all([ + Promise.allSettled(heights.map(h => getBlockByHeight(h))) + .then(results => results + .filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled') + .map(r => r.value) + ), getStaking(), getMining() ]); + console.log(`[SSR] Parallel fetch (blocks+staking+mining) took ${Date.now() - t2}ms`); + console.log(`[SSR] Total load time: ${Date.now() - t0}ms`); + return { chain, blocks, staking, mining }; } catch (e) { + console.error('[SSR] Error:', e); return { chain: null, blocks: [], staking: null, mining: null, error: String(e) }; } }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a18da03..585f66e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -23,7 +23,7 @@ { try { const block = await getBlock(params.hash); - return { block }; + + // Fetch addresses from all transactions + const addressSet = new Set(); + await Promise.all( + block.tx.map(async (txid) => { + try { + const tx = await getTransaction(txid); + // Extract addresses from outputs + tx.vout.forEach(out => { + if (out.scriptPubKey.addresses) { + out.scriptPubKey.addresses.forEach(addr => addressSet.add(addr)); + } + }); + } catch { + // Skip failed tx fetches + } + }) + ); + + return { + block, + addresses: Array.from(addressSet) + }; } catch { error(404, 'Block not found'); } diff --git a/src/routes/block/[hash]/+page.svelte b/src/routes/block/[hash]/+page.svelte index 3b58469..d3d6c7d 100644 --- a/src/routes/block/[hash]/+page.svelte +++ b/src/routes/block/[hash]/+page.svelte @@ -4,6 +4,7 @@ let { data } = $props(); const block = data.block; + const addresses = data.addresses || []; const fields: [string, string][] = [ ['Height', String(block.height)], @@ -56,6 +57,28 @@
+ +{#if addresses.length > 0} +
+
+

Addresses Involved ({addresses.length})

+
+
+
+ {#each addresses as addr} + + {addr} + + {/each} +
+
+
+{/if} +
diff --git a/src/routes/blocks/+page.server.ts b/src/routes/blocks/+page.server.ts index cdea72b..5b3c8cb 100644 --- a/src/routes/blocks/+page.server.ts +++ b/src/routes/blocks/+page.server.ts @@ -1,9 +1,13 @@ -import { getChainInfo, getBlockByHeight } from '$lib/api'; +import { getChainInfo, getBlockByHeight, getTransaction } from '$lib/api'; import type { PageServerLoad } from './$types'; import type { Block } from '$lib/types'; const PAGE_SIZE = 20; +interface BlockWithAddresses extends Block { + addresses?: string[]; +} + export const load: PageServerLoad = async ({ url }) => { const page = parseInt(url.searchParams.get('page') || '1'); @@ -11,10 +15,35 @@ export const load: PageServerLoad = async ({ url }) => { const chain = await getChainInfo(); const startHeight = chain.blocks - (page - 1) * PAGE_SIZE; - const blocks: Block[] = []; + const blocks: BlockWithAddresses[] = []; for (let i = 0; i < PAGE_SIZE && startHeight - i >= 0; i++) { try { - blocks.push(await getBlockByHeight(startHeight - i)); + const block = await getBlockByHeight(startHeight - i); + + // Fetch addresses from transactions (limit to first 50 txs to avoid slowdown) + const addressSet = new Set(); + const txsToFetch = block.tx.slice(0, 50); + + await Promise.all( + txsToFetch.map(async (txid) => { + try { + const tx = await getTransaction(txid); + // Extract addresses from outputs + tx.vout.forEach(out => { + if (out.scriptPubKey.addresses) { + out.scriptPubKey.addresses.forEach(addr => addressSet.add(addr)); + } + }); + } catch { + // Skip failed tx fetches + } + }) + ); + + blocks.push({ + ...block, + addresses: Array.from(addressSet) + }); } catch { break; } } diff --git a/src/routes/tx/[txid]/+page.svelte b/src/routes/tx/[txid]/+page.svelte index 187807f..e02575f 100644 --- a/src/routes/tx/[txid]/+page.svelte +++ b/src/routes/tx/[txid]/+page.svelte @@ -3,6 +3,15 @@ let { data } = $props(); const tx = data.tx; + + // Extract unique addresses from outputs + const addresses = new Set(); + tx.vout.forEach(out => { + if (out.scriptPubKey.addresses) { + out.scriptPubKey.addresses.forEach(addr => addresses.add(addr)); + } + }); + const uniqueAddresses = Array.from(addresses); @@ -43,6 +52,27 @@
+ +{#if uniqueAddresses.length > 0} +
+
+

Addresses Involved ({uniqueAddresses.length})

+
+
+
+ {#each uniqueAddresses as addr} + + {addr} + + {/each} +
+
+
+{/if} +