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;
}