Add transaction summaries to explorer tables
This commit is contained in:
+30
-1
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user