Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/address/[address]/components/AddressGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function AddressGraph({ address }: { address: string }) {
<div><dt className="text-muted">Indexed transactions</dt><dd className="mt-1 text-secondary font-mono">{selected.peer.txCount.toLocaleString('en-US')}</dd></div>
</> : null}
</dl>
<Link href={`/address/${selected.id}`} className="mt-5 inline-flex rounded border border-cipher-border px-3 py-2 text-xs font-mono text-primary hover:bg-cipher-hover">Open address →</Link>
<Link href={`/address/${selected.id}`} prefetch={false} className="mt-5 inline-flex rounded border border-cipher-border px-3 py-2 text-xs font-mono text-primary hover:bg-cipher-hover">Open address →</Link>
</div> : <div><h3 className="text-sm font-semibold text-primary">Inspect a connection</h3><p className="mt-2 text-xs leading-relaxed text-muted">Select a node or choose an address below. Its details stay open while you explore.</p></div>}
<div className="mt-5 border-t border-cipher-border pt-4">
<label htmlFor="connection-search" className="text-xs text-muted">Find a displayed address or label</label>
Expand Down
4 changes: 2 additions & 2 deletions app/address/[address]/components/AddressHeroCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export function AddressHeroCard({ data, priceData, crossChain, totalTxCount }: {
</dl>
</div>
{(funding || (crossChain && crossChain.totalSwaps > 0)) && <div className="flex flex-wrap gap-x-8 gap-y-3 border-t border-cipher-border px-5 sm:px-7 py-4 text-xs text-muted">
{funding && <p>First funding <Link href={`/tx/${funding.txid}`} className="ml-2 text-secondary underline underline-offset-4 hover:text-primary">{funding.amountZec.toLocaleString('en-US', { maximumFractionDigits: 8 })} {CURRENCY} ↗</Link>
{funding.isCoinbase ? ' · Mining reward' : funding.funderAddress ? <> · From <Link className="text-secondary hover:text-primary" href={`/address/${funding.funderAddress}`}>{funding.funderLabel || `${funding.funderAddress.slice(0, 6)}…${funding.funderAddress.slice(-4)}`}</Link></> : null}
{funding && <p>First funding <Link href={`/tx/${funding.txid}`} prefetch={false} className="ml-2 text-secondary underline underline-offset-4 hover:text-primary">{funding.amountZec.toLocaleString('en-US', { maximumFractionDigits: 8 })} {CURRENCY} ↗</Link>
{funding.isCoinbase ? ' · Mining reward' : funding.funderAddress ? <> · From <Link className="text-secondary hover:text-primary" href={`/address/${funding.funderAddress}`} prefetch={false}>{funding.funderLabel || `${funding.funderAddress.slice(0, 6)}…${funding.funderAddress.slice(-4)}`}</Link></> : null}
</p>}
{crossChain && crossChain.totalSwaps > 0 && <p>Bridge records <span className="ml-2 font-mono text-secondary">{crossChain.totalSwaps.toLocaleString('en-US')}</span> · {crossChain.entryCount} in / {crossChain.exitCount} out</p>}
</div>}
Expand Down
4 changes: 2 additions & 2 deletions app/address/[address]/components/AddressSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function AddressSummary({ data, totalTxCount }: AddressSummaryProps) {
if (funding.funderLabel && funding.funderAddress) {
return (
<Link
href={`/address/${funding.funderAddress}`}
href={`/address/${funding.funderAddress}`} prefetch={false}
className="text-cipher-gold hover:text-primary transition-colors"
>
{funding.funderLabel}
Expand All @@ -46,7 +46,7 @@ export function AddressSummary({ data, totalTxCount }: AddressSummaryProps) {
if (funding.funderAddress) {
return (
<Link
href={`/address/${funding.funderAddress}`}
href={`/address/${funding.funderAddress}`} prefetch={false}
className="text-cipher-gold hover:text-primary transition-colors"
>
{shortAddr(funding.funderAddress)}
Expand Down
2 changes: 1 addition & 1 deletion app/address/[address]/components/TransactionTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function TransactionTable({ address, data, currentPage, totalPages, pageS
if (other === 'shielded') return <TxTypeBadge category="shielded" />;
return other && other !== address ? <HashLink value={other} href={`/address/${other}`} lead={6} tail={4} copy={false} /> : <span className="text-xs text-muted">—</span>;
} },
{ id: 'block', header: 'Block', className: 'hidden md:table-cell', cell: tx => tx.blockHeight ? <Link href={`/block/${tx.blockHeight}`} className="text-xs font-mono text-secondary hover:text-primary">{tx.blockHeight.toLocaleString('en-US')}</Link> : <span className="text-xs text-muted">—</span> },
{ id: 'block', header: 'Block', className: 'hidden md:table-cell', cell: tx => tx.blockHeight ? <Link href={`/block/${tx.blockHeight}`} prefetch={false} className="text-xs font-mono text-secondary hover:text-primary">{tx.blockHeight.toLocaleString('en-US')}</Link> : <span className="text-xs text-muted">—</span> },
{ id: 'amount', header: `Balance change (${CURRENCY})`, align: 'right', cell: tx => <span className={`text-xs font-mono whitespace-nowrap ${tx.amount === 0 ? 'text-secondary' : tx.type === 'received' ? 'text-cipher-green' : 'text-danger'}`}>{tx.amount === 0 ? '' : tx.type === 'received' ? '+' : '−'}{Math.abs(tx.amount).toLocaleString('en-US', { maximumFractionDigits: 8 })}</span> },
{ id: 'age', header: 'Age', align: 'right', cell: tx => <RelativeTime timestamp={tx.timestamp} className="text-xs text-muted whitespace-nowrap" /> },
];
Expand Down
2 changes: 1 addition & 1 deletion app/address/[address]/components/UnifiedAddressViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function UnifiedAddressViewer({ uaComponents, uaLoading, copiedText, onCo
<div><TxTypeBadge category={receiver.key} /></div>
<div className="min-w-0">
{receiver.address ? <div className="flex items-start gap-2"><code className="min-w-0 break-all text-xs text-secondary">{receiver.address}</code><CopyButton text={receiver.address} label={receiver.key} copiedText={copiedText} onCopy={onCopy} /></div> : <p className="text-sm text-secondary">Receiver present · use the Unified Address</p>}
{receiver.key === 'transparent' && receiver.address ? <Link className="inline-flex mt-3 rounded border border-cipher-border px-3 py-2 text-xs font-mono text-primary hover:bg-cipher-hover" href={`/address/${receiver.address}`}>View transparent activity →</Link> : <p className="text-xs text-muted mt-2">Balance and history remain private.</p>}
{receiver.key === 'transparent' && receiver.address ? <Link className="inline-flex mt-3 rounded border border-cipher-border px-3 py-2 text-xs font-mono text-primary hover:bg-cipher-hover" href={`/address/${receiver.address}`} prefetch={false}>View transparent activity →</Link> : <p className="text-xs text-muted mt-2">Balance and history remain private.</p>}
</div>
</div>)}
{!receivers.length && <p className="p-5 text-sm text-muted">No supported receiver details were returned by the decoder.</p>}
Expand Down
12 changes: 7 additions & 5 deletions app/api/governance/announcement/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { getGovernanceCatalog } from '@/lib/governance-data';
import { selectAnnouncement } from '@/lib/governance';
import { announcementCacheHeaders, buildAnnouncementSnapshot } from '@/lib/governance-announcement';
import { getNetwork } from '@/lib/seo';

export async function GET() {
if (getNetwork() !== 'mainnet') return Response.json({ announcement: null }, { headers: { 'X-Robots-Tag': 'noindex' } });
if (getNetwork() !== 'mainnet') return Response.json(
{ announcement: null, generatedAt: Date.now(), nextChangeAt: null },
{ headers: announcementCacheHeaders(60) },
);
const catalog = await getGovernanceCatalog();
return Response.json({ announcement: selectAnnouncement(catalog, Date.now()) }, { headers: {
'Cache-Control': 'no-store', 'X-Robots-Tag': 'noindex',
} });
const { snapshot, ttl } = buildAnnouncementSnapshot(catalog, Date.now());
return Response.json(snapshot, { headers: announcementCacheHeaders(ttl) });
}
4 changes: 2 additions & 2 deletions app/block/[height]/FutureBlockView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function FutureBlockView({
)}
<div className="flex flex-wrap items-center justify-center gap-3">
<Link
href={`/block/${targetHeight}`}
href={`/block/${targetHeight}`} prefetch={false}
className="btn btn-md btn-primary"
>
View Block →
Expand Down Expand Up @@ -134,7 +134,7 @@ export function FutureBlockView({
<dl className="mt-6 grid grid-cols-1 gap-5 border-t border-cipher-border pt-6 sm:grid-cols-2 sm:gap-8">
<div>
<dt className="text-sm text-secondary mb-2">Latest block</dt>
<dd className="font-mono text-primary tabular-nums"><Link href={`/block/${currentHeight}`} className="hover:text-cipher-gold transition-colors">#{currentHeight.toLocaleString('en-US')}</Link></dd>
<dd className="font-mono text-primary tabular-nums"><Link href={`/block/${currentHeight}`} prefetch={false} className="hover:text-cipher-gold transition-colors">#{currentHeight.toLocaleString('en-US')}</Link></dd>
</div>
<div>
<dt className="text-sm text-secondary mb-2">{upgrade ? 'Activation block' : 'Upcoming block'}</dt>
Expand Down
2 changes: 1 addition & 1 deletion app/block/[height]/components/BlockPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
const disabled = !hash;
return (
<Link
href={disabled ? '#' : `/block/${height}`}
href={disabled ? '#' : `/block/${height}`} prefetch={false}
aria-disabled={disabled}
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-xs font-mono transition-colors ${
disabled
Expand Down Expand Up @@ -102,12 +102,12 @@
solution: data.solution,
totalFees: data.totalFees,
minerAddress: data.minerAddress,
transactions: data.transactions?.map((tx: any) => ({

Check warning on line 105 in app/block/[height]/components/BlockPageHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Next.js)

Unexpected any. Specify a different type
txid: tx.txid,
type: tx.vin?.[0]?.coinbase ? 'coinbase' : tx.hasShieldedActivity ? 'shielded' : 'regular',
inputs: tx.vin?.length || 0,
outputs: tx.vout?.length || 0,
amount: tx.vout?.reduce((sum: number, out: any) => sum + (out.value || 0), 0) || 0

Check warning on line 110 in app/block/[height]/components/BlockPageHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Next.js)

Unexpected any. Specify a different type
}))
}}
csvData={data.transactions}
Expand All @@ -115,12 +115,12 @@
type="both"
label="Export"
csvHeaders={['TXID', 'Type', 'Inputs', 'Outputs', 'Amount (ZEC)']}
csvMapper={(tx: any) => [

Check warning on line 118 in app/block/[height]/components/BlockPageHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Next.js)

Unexpected any. Specify a different type
tx.txid,
tx.vin?.[0]?.coinbase ? 'Coinbase' : tx.hasShieldedActivity ? 'Shielded' : 'Regular',
String(tx.vin?.length || 0),
String(tx.vout?.length || 0),
tx.vout?.reduce((sum: number, out: any) => sum + (out.value || 0), 0).toFixed(8) || '0'

Check warning on line 123 in app/block/[height]/components/BlockPageHeader.tsx

View workflow job for this annotation

GitHub Actions / Frontend (Next.js)

Unexpected any. Specify a different type
]}
/>
</div>
Expand Down
9 changes: 9 additions & 0 deletions app/block/[height]/components/BlockPageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Link from 'next/link';
import { SkeletonTable } from '@/components/ui/EmptyState';
import { Skeleton } from '@/components/ui/Skeleton';
import { Card, CardHeader, CardBody } from '@/components/ui/Card';
Expand Down Expand Up @@ -50,6 +51,14 @@ export function BlockPageSkeleton({
</>
)}
</p>
{initialSummary?.isOrphaned && initialSummary.canonicalReplacementHeight != null && (
<p className="mt-2 text-xs sm:text-sm text-secondary">
Canonical replacement at this height:{' '}
<Link href={`/block/${initialSummary.canonicalReplacementHeight}`} prefetch={false} className="text-brand-gold hover:underline">
Zcash block #{initialSummary.canonicalReplacementHeight.toLocaleString('en-US')}
</Link>.
</p>
)}
{/* Server-seeded facts from the SEO resolution fetch — real content
instead of a shimmer while the full block detail loads client-side. */}
{initialSummary && (initialSummary.timestamp != null || initialSummary.transactionCount != null || initialSummary.size != null) && (
Expand Down
2 changes: 2 additions & 0 deletions app/block/[height]/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface BlockPageSummary {
hash: string;
firstSeenAt?: string | null;
isOrphaned: boolean;
/** Present only when the API identifies a different canonical block at this height. */
canonicalReplacementHeight?: number | null;
/**
* Extra fields already available from the server-side block resolution
* (`getBlockResolution` in lib/seo.ts) used for SEO metadata. Threading
Expand Down
6 changes: 6 additions & 0 deletions app/block/[height]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getApiUrl,
getNetwork,
getBlockResolution,
BLOCK_HASH_PATTERN,
normalizeBlockIdentifier,
type BlockRecord,
} from '@/lib/seo';
Expand Down Expand Up @@ -92,6 +93,10 @@ export default async function BlockPage({
const height = Number(block.height);
const hash = block.hash.toLowerCase();
const isOrphaned = block.isOrphaned === true;
const replacementHash = block.canonicalBlock?.hash;
const canonicalReplacementHeight = isOrphaned && typeof replacementHash === 'string'
&& BLOCK_HASH_PATTERN.test(replacementHash) && replacementHash.toLowerCase() !== hash
? height : null;
const rawTransactionCount = block.transactionCount ?? block.transaction_count ?? null;
const transactionCount = rawTransactionCount === null || rawTransactionCount === undefined
? null
Expand All @@ -104,6 +109,7 @@ export default async function BlockPage({
height,
hash,
isOrphaned,
canonicalReplacementHeight,
timestamp: Number.isFinite(timestamp) ? timestamp : null,
transactionCount: Number.isFinite(transactionCount) ? transactionCount : null,
size: Number.isFinite(size) ? size : null,
Expand Down
2 changes: 1 addition & 1 deletion app/blocks/BlocksClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function blockColumns(blocks: Block[], trailingBlock: Block | null, schedule: Bl
skeletonWidth: 'w-24',
cell: (block) => (
<div className="flex items-center gap-2">
<Link href={`/block/${block.height}`} className="font-mono text-sm text-primary hover:text-primary transition-colors">
<Link href={`/block/${block.height}`} prefetch={false} className="font-mono text-sm text-primary hover:text-primary transition-colors">
{block.height.toLocaleString()}
</Link>
{block.finality_status === 'Finalized' && (
Expand Down
3 changes: 2 additions & 1 deletion app/finalizer/[pubkey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export default function FinalizerPage() {
<StakingActionBadge type={a.action_type} compact />
</td>
<td className="px-3 sm:px-4 py-3">
<Link href={`/block/${a.block_height}`} className="text-cipher-gold hover:underline font-mono">
<Link href={`/block/${a.block_height}`} prefetch={false} className="text-cipher-gold hover:underline font-mono">
#{a.block_height.toLocaleString()}
</Link>
</td>
Expand Down Expand Up @@ -609,6 +609,7 @@ function DelegatorsPanel({ actions }: { actions: StakeAction[] }) {
<td className="px-3 sm:px-4 py-3">
<Link
href={`/block/${d.lastActionHeight}`}
prefetch={false}
className="text-cipher-gold hover:underline font-mono"
>
#{d.lastActionHeight.toLocaleString()}
Expand Down
2 changes: 1 addition & 1 deletion app/governance/HowToVote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function HowToVote({ snapshotHeight }: { snapshotHeight?: number }) {
<h3 className="text-sm font-medium text-primary"><span className="mr-2 font-mono text-cipher-gold">02</span>Check your eligible balance</h3>
<p className="mt-2 text-sm leading-relaxed text-secondary">Follow the wallet’s eligibility check. Each poll uses a snapshot: an earlier point in the blockchain used to determine which funds can vote.</p>
{snapshotHeight !== undefined && (
<Link href={`/block/${snapshotHeight}`} className="mt-2 inline-block text-xs text-cipher-gold hover:underline">
<Link href={`/block/${snapshotHeight}`} prefetch={false} className="mt-2 inline-block text-xs text-cipher-gold hover:underline">
This poll’s snapshot: block {snapshotHeight.toLocaleString('en-US')} →
</Link>
)}
Expand Down
2 changes: 1 addition & 1 deletion app/governance/nu7/NU7VoteClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export function NU7VoteClient({ initialData, resultsState, resultsContent, initi
{countdownLabel}
</span>
{phase === 'pre-snapshot' && (
<Link href={`/block/${NU7_VOTE.snapshotHeight}`} className="text-caption font-mono text-muted hover:text-primary transition-colors ml-auto">
<Link href={`/block/${NU7_VOTE.snapshotHeight}`} prefetch={false} className="text-caption font-mono text-muted hover:text-primary transition-colors ml-auto">
Block #{NU7_VOTE.snapshotHeight.toLocaleString()}
</Link>
)}
Expand Down
6 changes: 3 additions & 3 deletions app/name/[name]/NameClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function RegisteredView({
<Field label="Resolves to">
<div className="flex items-center">
<Link
href={`/address/${registration.address}`}
href={`/address/${registration.address}`} prefetch={false}
className="font-mono text-sm hover:text-primary transition-colors break-all"
>
{registration.address}
Expand All @@ -128,7 +128,7 @@ function RegisteredView({
<Field label="Block">
<div className="flex items-center">
<Link
href={`/block/${registration.height}`}
href={`/block/${registration.height}`} prefetch={false}
className="font-mono text-sm hover:text-primary transition-colors"
>
{registration.height.toLocaleString('en-US')}
Expand Down Expand Up @@ -191,7 +191,7 @@ function RegisteredView({
</td>
<td className="py-2 pr-4 font-mono">
<Link
href={`/block/${e.height}`}
href={`/block/${e.height}`} prefetch={false}
className="hover:text-primary transition-colors"
>
{e.height.toLocaleString('en-US')}
Expand Down
2 changes: 1 addition & 1 deletion app/network/NetworkClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default function NetworkClient({ initialData }: { initialData: NetworkPag
</dl>
<dl id="network-overview" className="network-section network-summary-grid network-live-facts" aria-label="Current chain activity">
{[
{ label: 'Latest block', value: height != null ? <Link href={`/block/${height}`} className="hover:text-cipher-gold">{height.toLocaleString()}</Link> : '—', hint: stats ? `Block timestamp · ${blockAgeLabel(stats.blockchain.latestBlockTime, now)}` : 'Awaiting chain data' },
{ label: 'Latest block', value: height != null ? <Link href={`/block/${height}`} prefetch={false} className="hover:text-cipher-gold">{height.toLocaleString()}</Link> : '—', hint: stats ? `Block timestamp · ${blockAgeLabel(stats.blockchain.latestBlockTime, now)}` : 'Awaiting chain data' },
{ label: 'Average block interval', value: stats?.mining.avgBlockTime != null ? `${stats.mining.avgBlockTime.toFixed(1)}s` : '—', hint: stats?.mining.targetBlockTime != null ? `Observed · protocol target ${stats.mining.targetBlockTime}s` : 'Observed · protocol target unavailable' },
{ label: 'Transactions · 24h', value: txCount?.toLocaleString() ?? '—', hint: stats?.blockchain.tx24hExclCoinbase != null ? 'Confirmed · coinbase excluded' : 'Confirmed · includes coinbase' },
{ label: 'Estimated hashrate · 24h', value: stats?.mining.hashrateEstimate ? stats.mining.networkHashrate : '—', hint: <Link href="/mining#metrics" className="hover:text-primary underline underline-offset-4">Trailing 24-hour estimate →</Link> },
Expand Down
2 changes: 1 addition & 1 deletion app/network/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default async function NetworkPage() {
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12">
<div className="border-t border-cipher-border pt-6 max-w-3xl text-sm text-muted space-y-2">
<h2 className="font-mono font-medium text-secondary">About these observations</h2>
{nu7Height != null && <p>The serving node announces NU7 at <Link href={`/block/${nu7Height}`} className="text-brand-gold hover:underline">block #{nu7Height.toLocaleString('en-US')}</Link> on Zcash {network}. {nu7Active ? <>The upgrade is active; explore <Link href="#nu7-accounting" className="text-brand-gold hover:underline">fee allocation and NSM accounting</Link> below.</> : <>View its canonical block or the estimated activation countdown.</>}</p>}
{nu7Height != null && <p>The serving node announces NU7 at <Link href={`/block/${nu7Height}`} prefetch={false} className="text-brand-gold hover:underline">block #{nu7Height.toLocaleString('en-US')}</Link> on Zcash {network}. {nu7Active ? <>The upgrade is active; explore <Link href="#nu7-accounting" className="text-brand-gold hover:underline">fee allocation and NSM accounting</Link> below.</> : <>View its canonical block or the estimated activation countdown.</>}</p>}
<p>Chain statistics, recent blocks and node discovery are separate observations and can update at different times. Block cadence uses timestamps recorded in blocks; these are not measurements of when this explorer received them.</p>
<p>The map shows observed reachable nodes, not a census of every Zcash node. Peer connections and disk usage in Technical details describe this explorer’s node. Observed transaction fees describe past transactions and are not fee recommendations.</p>
</div>
Expand Down
Loading
Loading