diff --git a/frontend/src/components/StreamDetailDrawer.test.tsx b/frontend/src/components/StreamDetailDrawer.test.tsx index d1534ab..1890f55 100644 --- a/frontend/src/components/StreamDetailDrawer.test.tsx +++ b/frontend/src/components/StreamDetailDrawer.test.tsx @@ -403,3 +403,59 @@ describe('StreamDetailDrawer', () => { }); }); }); + +// ── Stellar Expert transaction links (#399) ────────────────────────────────── + +describe('TxHashLink — Stellar Expert transaction links (#399)', () => { + beforeEach(() => { + onClose.mockClear(); + clearCache(); + }); + + it('renders txHash as a link to Stellar Expert testnet', async () => { + render(); + await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument()); + + const link = screen.getByRole('link', { name: /View transaction.*Stellar Expert/i }); + expect(link).toHaveAttribute( + 'href', + 'https://stellar.expert/explorer/testnet/tx/abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab', + ); + }); + + it('link opens in a new tab with rel=noopener noreferrer', async () => { + render(); + await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument()); + + const link = screen.getByRole('link', { name: /View transaction/i }); + expect(link).toHaveAttribute('target', '_blank'); + expect(link).toHaveAttribute('rel', 'noopener noreferrer'); + }); + + it('displays truncated hash (first 8 + last 8 chars)', async () => { + render(); + await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument()); + + // full: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab + expect(screen.getByText('abcdef12\u2026567890ab')).toBeInTheDocument(); + }); + + it('full hash is in the tooltip (title attribute)', async () => { + render(); + await waitFor(() => expect(screen.getByText('Tokens claimed')).toBeInTheDocument()); + + const link = screen.getByRole('link', { name: /View transaction/i }); + expect(link).toHaveAttribute( + 'title', + 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab', + ); + }); + + it('does not render a tx link when txHash is absent', async () => { + render(); + await waitFor(() => expect(screen.getByText('Stream created')).toBeInTheDocument()); + // "created" event has no txHash — only the "claimed" event has a link + const links = screen.queryAllByRole('link', { name: /View transaction/i }); + expect(links.length).toBeLessThanOrEqual(1); + }); +}); diff --git a/frontend/src/components/StreamDetailDrawer.tsx b/frontend/src/components/StreamDetailDrawer.tsx index 52c9e05..43bca26 100644 --- a/frontend/src/components/StreamDetailDrawer.tsx +++ b/frontend/src/components/StreamDetailDrawer.tsx @@ -3,6 +3,28 @@ import { Stream } from "../types/stream"; import { StreamEvent, getStream, getStreamHistory } from "../services/api"; import { CopyableAddress } from "./CopyableAddress"; +const STELLAR_EXPERT_BASE = "https://stellar.expert/explorer/testnet/tx"; + +/** + * Renders a transaction hash as a truncated, clickable link to Stellar Expert. + * Shows first 8 + last 8 characters with the full hash in a tooltip. + */ +export function TxHashLink({ txHash }: { txHash: string }) { + const truncated = `${txHash.slice(0, 8)}…${txHash.slice(-8)}`; + return ( + + {truncated} + + ); +} + interface StreamDetailDrawerProps { streamId: string; /** Called when the drawer should close */ @@ -472,6 +494,9 @@ export function StreamDetailDrawer({ {evt.amount != null && ( · {evt.amount} tokens )} + {evt.txHash && ( + · + )} diff --git a/frontend/src/handlers.ts b/frontend/src/handlers.ts index 0b112ea..c83cdfc 100644 --- a/frontend/src/handlers.ts +++ b/frontend/src/handlers.ts @@ -62,6 +62,7 @@ export const handlers = [ timestamp: 1700010000, actor: 'GRECIPIENT456', amount: 100, + txHash: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab', }, ], }); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 4d1f261..ee6ed0d 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -287,6 +287,7 @@ export interface StreamEvent { timestamp: number; actor?: string; amount?: number; + txHash?: string; metadata?: Record; }