diff --git a/invofi/apps/frontend/src/app/portfolio/page.tsx b/invofi/apps/frontend/src/app/portfolio/page.tsx
index 0e41c59c3..41e322b76 100644
--- a/invofi/apps/frontend/src/app/portfolio/page.tsx
+++ b/invofi/apps/frontend/src/app/portfolio/page.tsx
@@ -19,6 +19,7 @@ import { stroopsToUsd } from '@/lib/live/prices';
import { useLivePortfolio } from '@/components/portfolio/LivePortfolioProvider';
import { ConnectionStatus } from '@/components/portfolio/ConnectionStatus';
import { RepaymentProgress } from '@/components/portfolio/RepaymentProgress';
+import { ExplorerLink } from '@/components/common/ExplorerLink';
import type { LivePosition } from '@/lib/live/types';
@@ -296,14 +297,13 @@ function PositionCard({ offer }: { offer: LivePosition }) {
{interestRateLabel(offer.interest_rate)} · {durationLabel(offer.duration)}
diff --git a/invofi/apps/frontend/src/components/common/ExplorerLink.test.tsx b/invofi/apps/frontend/src/components/common/ExplorerLink.test.tsx
new file mode 100644
index 000000000..1a82b81a7
--- /dev/null
+++ b/invofi/apps/frontend/src/components/common/ExplorerLink.test.tsx
@@ -0,0 +1,54 @@
+import { describe, expect, it } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { ExplorerLink } from './ExplorerLink';
+
+describe('ExplorerLink', () => {
+ it('renders a contract link with the correct href', () => {
+ render();
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute(
+ 'href',
+ 'https://stellar.expert/explorer/testnet/contract/abc123',
+ );
+ expect(link).toHaveAttribute('target', '_blank');
+ expect(link).toHaveAttribute('rel', 'noreferrer noopener');
+ });
+
+ it('renders an account link', () => {
+ render();
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute(
+ 'href',
+ 'https://stellar.expert/explorer/testnet/account/GABCDEF123456',
+ );
+ });
+
+ it('renders a transaction link', () => {
+ render();
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute(
+ 'href',
+ 'https://stellar.expert/explorer/testnet/tx/txhash',
+ );
+ });
+
+ it('renders children instead of the raw value', () => {
+ render(View);
+ expect(screen.getByRole('link')).toHaveTextContent('View');
+ });
+
+ it('applies a custom className', () => {
+ render();
+ expect(screen.getByRole('link')).toHaveClass('font-mono');
+ });
+
+ it('defaults title to "View on Stellar Expert"', () => {
+ render();
+ expect(screen.getByRole('link')).toHaveAttribute('title', 'View on Stellar Expert');
+ });
+
+ it('accepts a custom title', () => {
+ render();
+ expect(screen.getByRole('link')).toHaveAttribute('title', 'Check it out');
+ });
+});
\ No newline at end of file
diff --git a/invofi/apps/frontend/src/components/common/ExplorerLink.tsx b/invofi/apps/frontend/src/components/common/ExplorerLink.tsx
new file mode 100644
index 000000000..67d168c92
--- /dev/null
+++ b/invofi/apps/frontend/src/components/common/ExplorerLink.tsx
@@ -0,0 +1,41 @@
+import { explorerContractUrl, explorerAccountUrl, explorerTxUrl } from '@/lib/constants';
+
+interface ExplorerLinkProps {
+ type: 'contract' | 'account' | 'tx';
+ value: string;
+ children?: React.ReactNode;
+ className?: string;
+ title?: string;
+}
+
+/**
+ * Reusable Stellar Expert explorer link.
+ * Uses the network-aware helpers from `@/lib/constants` so testnet vs mainnet
+ * is handled in one place — no inline `STELLAR_EXPERT` constants in call sites.
+ */
+export function ExplorerLink({
+ type,
+ value,
+ children,
+ className = '',
+ title,
+}: ExplorerLinkProps) {
+ const url =
+ type === 'contract'
+ ? explorerContractUrl(value)
+ : type === 'account'
+ ? explorerAccountUrl(value)
+ : explorerTxUrl(value);
+
+ return (
+
+ {children ?? value}
+
+ );
+}
\ No newline at end of file
diff --git a/invofi/apps/frontend/src/components/layout/Footer.tsx b/invofi/apps/frontend/src/components/layout/Footer.tsx
index 572bf60a0..f6092a9bc 100644
--- a/invofi/apps/frontend/src/components/layout/Footer.tsx
+++ b/invofi/apps/frontend/src/components/layout/Footer.tsx
@@ -1,6 +1,7 @@
import Link from 'next/link';
import { useTranslations } from 'next-intl';
-import { REGISTRY_CONTRACT_ID, STELLAR_NETWORK, explorerContractUrl } from '@/lib/constants';
+import { ExplorerLink } from '@/components/common/ExplorerLink';
+import { REGISTRY_CONTRACT_ID, STELLAR_NETWORK } from '@/lib/constants';
import { Heart } from 'lucide-react';
const SITE_MAP = {
@@ -107,15 +108,14 @@ export function Footer() {
{REGISTRY_CONTRACT_ID && (
{t('contractOnStellar', { network: STELLAR_NETWORK })}{' '}
-
{REGISTRY_CONTRACT_ID.slice(0, 8)}…{REGISTRY_CONTRACT_ID.slice(-8)}
-
+
)}
diff --git a/invofi/apps/frontend/src/components/marketplace/MarketplaceCard.tsx b/invofi/apps/frontend/src/components/marketplace/MarketplaceCard.tsx
index 20a5102ee..e5f3a6e4f 100644
--- a/invofi/apps/frontend/src/components/marketplace/MarketplaceCard.tsx
+++ b/invofi/apps/frontend/src/components/marketplace/MarketplaceCard.tsx
@@ -1,13 +1,12 @@
import Link from 'next/link';
import { Calendar, DollarSign, ArrowRight, ExternalLink, Clock, AlertTriangle } from 'lucide-react';
+import { ExplorerLink } from '@/components/common/ExplorerLink';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { formatAmount, formatDate, formatAddress, INVOICE_STATUS_COLORS } from '@/lib/utils';
import type { Invoice } from '@/types';
-const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet' ? 'mainnet' : 'testnet';
-const STELLAR_EXPERT = `https://stellar.expert/explorer/${NETWORK}`;
function DueLabel({ dueDateUnix }: { dueDateUnix: number }) {
const now = Date.now() / 1000;
@@ -47,16 +46,14 @@ export function MarketplaceCard({ invoice }: MarketplaceCardProps) {
@@ -76,14 +73,13 @@ export function MarketplaceCard({ invoice }: MarketplaceCardProps) {
Originator:{' '}
-
{formatAddress(invoice.originator)}
-
+
diff --git a/invofi/apps/frontend/src/components/marketplace/PositionListingCard.tsx b/invofi/apps/frontend/src/components/marketplace/PositionListingCard.tsx
index 7195496fe..df955722c 100644
--- a/invofi/apps/frontend/src/components/marketplace/PositionListingCard.tsx
+++ b/invofi/apps/frontend/src/components/marketplace/PositionListingCard.tsx
@@ -7,11 +7,10 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { LISTING_STATUS_COLORS, unitPrice } from '@/lib/listings';
+import { ExplorerLink } from '@/components/common/ExplorerLink';
import { formatAddress } from '@/lib/utils';
import type { PositionListing, PositionListingStatus } from '@/types';
-const NETWORK = process.env.NEXT_PUBLIC_STELLAR_NETWORK === 'mainnet' ? 'mainnet' : 'testnet';
-const STELLAR_EXPERT = `https://stellar.expert/explorer/${NETWORK}`;
interface PositionListingCardProps {
listing: PositionListing;
@@ -71,14 +70,13 @@ export function PositionListingCard({ listing, isOwn, onStatusChange, busy }: Po