Add transaction summaries to explorer tables

This commit is contained in:
2026-03-25 21:40:55 -07:00
parent cd20c6c12e
commit e4722ed340
9 changed files with 124 additions and 23 deletions
+30 -1
View File
@@ -2,7 +2,7 @@ import { env } from '$env/dynamic/private';
import type {
ChainInfo, Block, BlockHeader, Transaction, Difficulty,
StakingInfo, MiningInfo, NetworkInfo, PeerInfo,
AddressBalance, AddressUtxo, SupplyInfo, ValidationResult
AddressBalance, AddressUtxo, SupplyInfo, ValidationResult, TransactionSummary
} from './types';
const BASE_URL = env.TRIANGLES_API_URL || 'http://127.0.0.1:19112';
@@ -95,6 +95,35 @@ export const getAddressTxids = (addr: string, start?: number, end?: number) => {
// Validation
export const validateAddress = (addr: string) => get<ValidationResult>(`validate/${addr}`);
function summarizeTransaction(tx: Transaction): TransactionSummary {
return {
txid: tx.txid,
time: tx.blocktime ?? tx.time,
confirmations: tx.confirmations,
outputCount: tx.vout.length,
totalOutput: tx.vout.reduce((sum, vout) => sum + vout.value, 0),
primaryAddress: tx.vout.flatMap((vout) => vout.scriptPubKey.addresses ?? [])[0],
isReward: tx.vin.some((vin) => Boolean(vin.coinbase))
};
}
export async function getTransactionSummaries(
txids: string[],
limit = txids.length
): Promise<TransactionSummary[]> {
const selected = limit > 0 ? txids.slice(0, limit) : txids;
return Promise.all(
selected.map(async (txid) => {
try {
return summarizeTransaction(await getTransaction(txid));
} catch {
return { txid };
}
})
);
}
// Helper: get latest N blocks (parallel fetch for performance)
export async function getLatestBlocks(count: number): Promise<Block[]> {
const chain = await getChainInfo();
+56 -6
View File
@@ -1,8 +1,19 @@
<script lang="ts">
import { truncateHash } from '$lib/utils';
import type { TransactionSummary } from '$lib/types';
import { formatAmount, formatTimestamp, truncateHash } from '$lib/utils';
let { txids, limit = 0 }: { txids: string[]; limit?: number } = $props();
const displayed = limit > 0 ? txids.slice(0, limit) : txids;
type Props = {
txs?: TransactionSummary[];
txids?: string[];
limit?: number;
};
let { txs = [], txids = [], limit = 0 }: Props = $props();
function displayed(): TransactionSummary[] {
const source = txs.length > 0 ? txs : txids.map((txid) => ({ txid }));
return limit > 0 ? source.slice(0, limit) : source;
}
</script>
<div class="overflow-x-auto">
@@ -11,16 +22,55 @@
<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>
<th class="text-left py-3 px-3">Time</th>
<th class="text-right py-3 px-3">Confirmations</th>
<th class="text-right py-3 px-3">Outputs</th>
<th class="text-right py-3 px-3">Total Output</th>
</tr>
</thead>
<tbody>
{#each displayed as txid, i}
{#each displayed() as tx, 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 href="/tx/{tx.txid}" class="text-tri-accent hover:text-tri-accent-light">
{truncateHash(tx.txid, 16)}
</a>
{#if tx.primaryAddress}
<div class="mt-1 text-tri-muted">
{#if tx.isReward}
Reward to
{:else}
First output
{/if}
{' '}
{truncateHash(tx.primaryAddress, 10)}
</div>
{/if}
</td>
<td class="py-2.5 px-3 text-xs text-tri-muted whitespace-nowrap">
{#if tx.time != null}
{formatTimestamp(tx.time)}
{:else}
<span class="text-tri-muted/60">Unavailable</span>
{/if}
</td>
<td class="py-2.5 px-3 text-right">
{#if tx.confirmations != null}
<span class="font-mono text-xs">{tx.confirmations}</span>
{:else}
<span class="text-tri-yellow text-xs">Pending</span>
{/if}
</td>
<td class="py-2.5 px-3 text-right font-mono text-xs">
{tx.outputCount ?? '—'}
</td>
<td class="py-2.5 px-3 text-right font-mono text-xs">
{#if tx.totalOutput != null}
<span class="text-tri-green">{formatAmount(tx.totalOutput)} TRI</span>
{:else}
<span class="text-tri-muted/60">Unavailable</span>
{/if}
</td>
</tr>
{/each}
+10
View File
@@ -59,6 +59,16 @@ export interface Transaction {
blocktime?: number;
}
export interface TransactionSummary {
txid: string;
time?: number;
confirmations?: number;
outputCount?: number;
totalOutput?: number;
primaryAddress?: string;
isReward?: boolean;
}
export interface TxInput {
txid?: string;
vout?: number;
+12 -3
View File
@@ -1,7 +1,15 @@
import { getAddressBalance, getAddressUtxos, getAddressTxids, validateAddress } from '$lib/api';
import {
getAddressBalance,
getAddressUtxos,
getAddressTxids,
getTransactionSummaries,
validateAddress
} from '$lib/api';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const TX_HISTORY_LIMIT = 50;
export const load: PageServerLoad = async ({ params }) => {
try {
const [validation, balance, utxos, txids] = await Promise.all([
@@ -15,11 +23,12 @@ export const load: PageServerLoad = async ({ params }) => {
error(400, 'Invalid address');
}
return { address: params.addr, balance, utxos, txids };
const txs = await getTransactionSummaries(txids, TX_HISTORY_LIMIT);
return { address: params.addr, balance, utxos, txs, totalTxs: txids.length };
} 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 };
return { address: params.addr, balance: null, utxos: [], txs: [], totalTxs: 0, indexError: true };
}
error(404, 'Address not found');
}
+5 -5
View File
@@ -68,13 +68,13 @@
<!-- 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>
<h2 class="text-white font-semibold">Transactions ({data.totalTxs})</h2>
</div>
{#if data.txids.length > 0}
<TxTable txids={data.txids} limit={50} />
{#if data.txids.length > 50}
{#if data.txs.length > 0}
<TxTable txs={data.txs} />
{#if data.totalTxs > 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
Showing 50 of {data.totalTxs} transactions
</div>
{/if}
{:else}
+3 -1
View File
@@ -1,4 +1,5 @@
import { getBlock, getTransaction } from '$lib/api';
import { getBlock, getTransactionSummaries } from '$lib/api';
import { getTransaction } from '$lib/api';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
@@ -26,6 +27,7 @@ export const load: PageServerLoad = async ({ params }) => {
return {
block,
txs: await getTransactionSummaries(block.tx),
addresses: Array.from(addressSet)
};
} catch {
+1 -1
View File
@@ -84,5 +84,5 @@
<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} />
<TxTable txs={data.txs} />
</div>
+4 -3
View File
@@ -1,11 +1,12 @@
import { getMempool } from '$lib/api';
import { getMempool, getTransactionSummaries } from '$lib/api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
try {
const txids = await getMempool();
return { txids };
const txs = await getTransactionSummaries(txids);
return { txs, totalTxs: txids.length };
} catch (e) {
return { txids: [], error: String(e) };
return { txs: [], totalTxs: 0, error: String(e) };
}
};
+3 -3
View File
@@ -10,12 +10,12 @@
<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>
<p class="text-tri-muted text-sm mt-1">{data.totalTxs} unconfirmed transaction{data.totalTxs !== 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} />
{#if data.txs.length > 0}
<TxTable txs={data.txs} />
{:else}
<div class="p-8 text-center text-tri-muted">Mempool is empty</div>
{/if}