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
131 changes: 131 additions & 0 deletions apps/dashboard/recovery/stellar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React, { useState, useMemo } from 'react';
import { FailedTransfer, RecoveryFilter } from './types';

const MOCK_FAILED_TRANSFERS: FailedTransfer[] = [
{ id: '1', transferHash: 'a1b2c3d4e5f6', sourceChain: 'Stellar', destinationChain: 'Ethereum', asset: 'USDC', amount: '1500.00', failureReason: 'Sequence mismatch', failedAt: '2025-06-27T10:30:00Z', recoveryStatus: 'recovered', retryCount: 2, lastRetryAt: '2025-06-27T10:35:00Z' },
{ id: '2', transferHash: 'b2c3d4e5f6a7', sourceChain: 'Stellar', destinationChain: 'Polygon', asset: 'USDT', amount: '2500.00', failureReason: 'Insufficient liquidity', failedAt: '2025-06-27T11:00:00Z', recoveryStatus: 'pending', retryCount: 0 },
{ id: '3', transferHash: 'c3d4e5f6a7b8', sourceChain: 'Ethereum', destinationChain: 'Stellar', asset: 'ETH', amount: '12.50', failureReason: 'Gas estimation failed', failedAt: '2025-06-27T11:15:00Z', recoveryStatus: 'in_progress', retryCount: 1, lastRetryAt: '2025-06-27T11:20:00Z' },
{ id: '4', transferHash: 'd4e5f6a7b8c9', sourceChain: 'Stellar', destinationChain: 'Base', asset: 'XLM', amount: '5000.00', failureReason: 'Network timeout', failedAt: '2025-06-27T09:45:00Z', recoveryStatus: 'failed', retryCount: 3, lastRetryAt: '2025-06-27T10:00:00Z' },
{ id: '5', transferHash: 'e5f6a7b8c9d0', sourceChain: 'Polygon', destinationChain: 'Stellar', asset: 'USDC', amount: '800.00', failureReason: 'Contract revert', failedAt: '2025-06-27T08:30:00Z', recoveryStatus: 'recovered', retryCount: 1, lastRetryAt: '2025-06-27T08:35:00Z' },
];

const STATUS_COLORS: Record<string, string> = {
pending: '#f59e0b',
in_progress: '#3b82f6',
recovered: '#22c55e',
failed: '#ef4444',
};

function StatusBadge({ status }: { status: string }) {
const color = STATUS_COLORS[status] || '#64748b';
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '2px 8px', borderRadius: 12, fontSize: 12, fontWeight: 600, background: `${color}18`, color }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: color }} />
{status.replace('_', ' ')}
</span>
);
}

function SummaryCard({ label, value, color }: { label: string; value: number; color: string }) {
return (
<div style={{ padding: '16px 20px', border: '1px solid #e2e8f0', borderRadius: 8, background: '#f8fafc', minWidth: 120, textAlign: 'center' }}>
<div style={{ fontSize: 28, fontWeight: 700, color }}>{value}</div>
<div style={{ fontSize: 12, color: '#64748b', marginTop: 4 }}>{label}</div>
</div>
);
}

export default function StellarTransferRecoveryDashboard() {
const [transfers] = useState<FailedTransfer[]>(MOCK_FAILED_TRANSFERS);
const [filter, setFilter] = useState<RecoveryFilter>({});

const filtered = useMemo(() => {
return transfers.filter((t) => {
if (filter.status && t.recoveryStatus !== filter.status) return false;
if (filter.sourceChain && t.sourceChain !== filter.sourceChain) return false;
if (filter.asset && t.asset !== filter.asset) return false;
return true;
});
}, [transfers, filter]);

const summary = useMemo(() => {
return {
total: filtered.length,
pending: filtered.filter((t) => t.recoveryStatus === 'pending').length,
inProgress: filtered.filter((t) => t.recoveryStatus === 'in_progress').length,
recovered: filtered.filter((t) => t.recoveryStatus === 'recovered').length,
failed: filtered.filter((t) => t.recoveryStatus === 'failed').length,
};
}, [filtered]);

const chains = Array.from(new Set(transfers.map((t) => t.sourceChain)));
const assets = Array.from(new Set(transfers.map((t) => t.asset)));
const statuses = ['pending', 'in_progress', 'recovered', 'failed'];

return (
<div style={{ padding: 24, fontFamily: 'system-ui, sans-serif', maxWidth: 1200, margin: '0 auto' }}>
<h1 style={{ fontSize: 24, fontWeight: 700, color: '#0f172a', marginBottom: 4 }}>
Stellar Transfer Recovery Dashboard
</h1>
<p style={{ color: '#64748b', marginBottom: 24 }}>
Monitor and manage failed transfer recovery operations.
</p>

<div style={{ display: 'flex', gap: 12, marginBottom: 24 }}>
<SummaryCard label="Total" value={summary.total} color="#0f172a" />
<SummaryCard label="Pending" value={summary.pending} color="#f59e0b" />
<SummaryCard label="In Progress" value={summary.inProgress} color="#3b82f6" />
<SummaryCard label="Recovered" value={summary.recovered} color="#22c55e" />
<SummaryCard label="Failed" value={summary.failed} color="#ef4444" />
</div>

<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 24 }}>
<select style={{ padding: '6px 10px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13 }} value={filter.status ?? ''} onChange={(e) => setFilter((f) => ({ ...f, status: e.target.value || undefined }))}>
<option value="">All statuses</option>
{statuses.map((s) => <option key={s} value={s}>{s.replace('_', ' ')}</option>)}
</select>
<select style={{ padding: '6px 10px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13 }} value={filter.sourceChain ?? ''} onChange={(e) => setFilter((f) => ({ ...f, sourceChain: e.target.value || undefined }))}>
<option value="">All chains</option>
{chains.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
<select style={{ padding: '6px 10px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13 }} value={filter.asset ?? ''} onChange={(e) => setFilter((f) => ({ ...f, asset: e.target.value || undefined }))}>
<option value="">All assets</option>
{assets.map((a) => <option key={a} value={a}>{a}</option>)}
</select>
<button style={{ padding: '6px 12px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13, cursor: 'pointer', background: '#fff' }} onClick={() => setFilter({})} type="button">Clear filters</button>
</div>

<div style={{ border: '1px solid #e2e8f0', borderRadius: 8, overflow: 'hidden' }}>
<div style={{ padding: '14px 16px', background: '#f1f5f9', borderBottom: '1px solid #e2e8f0' }}>
<h2 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: '#1e293b' }}>Failed Transfers</h2>
</div>
{filtered.length === 0 ? (
<div style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>No transfers match the filters.</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f8fafc' }}>
{['Transfer Hash', 'Route', 'Asset', 'Amount', 'Failure Reason', 'Status', 'Retries'].map((h) => (
<th key={h} style={{ padding: '10px 12px', textAlign: 'left', fontSize: 13, color: '#64748b', fontWeight: 600, whiteSpace: 'nowrap' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{filtered.map((t) => (
<tr key={t.id} style={{ borderTop: '1px solid #e2e8f0' }}>
<td style={{ padding: '10px 12px', fontFamily: 'monospace', fontSize: 12 }}>{t.transferHash.slice(0, 12)}...</td>
<td style={{ padding: '10px 12px', fontWeight: 500 }}>{t.sourceChain} &rarr; {t.destinationChain}</td>
<td style={{ padding: '10px 12px' }}>{t.asset}</td>
<td style={{ padding: '10px 12px' }}>{t.amount}</td>
<td style={{ padding: '10px 12px', color: '#ef4444', fontSize: 13 }}>{t.failureReason}</td>
<td style={{ padding: '10px 12px' }}><StatusBadge status={t.recoveryStatus} /></td>
<td style={{ padding: '10px 12px', fontSize: 13 }}>{t.retryCount}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}
27 changes: 27 additions & 0 deletions apps/dashboard/recovery/stellar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface FailedTransfer {
id: string;
transferHash: string;
sourceChain: string;
destinationChain: string;
asset: string;
amount: string;
failureReason: string;
failedAt: string;
recoveryStatus: 'pending' | 'in_progress' | 'recovered' | 'failed';
retryCount: number;
lastRetryAt?: string;
}

export interface RecoverySummary {
total: number;
pending: number;
inProgress: number;
recovered: number;
failed: number;
}

export interface RecoveryFilter {
status?: string;
sourceChain?: string;
asset?: string;
}
119 changes: 119 additions & 0 deletions apps/explorer/assets/stellar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useState, useMemo } from 'react';

interface BridgeableAsset {
id: string;
symbol: string;
name: string;
icon?: string;
chains: string[];
totalLiquidityUsd: number;
avgFeeUsd: number;
bridgeProviders: string[];
tags: string[];
}

const MOCK_ASSETS: BridgeableAsset[] = [
{ id: '1', symbol: 'USDC', name: 'USD Coin', chains: ['Stellar', 'Ethereum', 'Polygon', 'Base'], totalLiquidityUsd: 125000000, avgFeeUsd: 0.80, bridgeProviders: ['AllBridge', 'Squid', 'Wormhole'], tags: ['stablecoin'] },
{ id: '2', symbol: 'USDT', name: 'Tether USD', chains: ['Stellar', 'Ethereum', 'Polygon'], totalLiquidityUsd: 98000000, avgFeeUsd: 0.75, bridgeProviders: ['AllBridge', 'Squid'], tags: ['stablecoin'] },
{ id: '3', symbol: 'XLM', name: 'Stellar Lumens', chains: ['Stellar', 'Base', 'Ethereum'], totalLiquidityUsd: 45000000, avgFeeUsd: 0.30, bridgeProviders: ['AllBridge', 'Stargate'], tags: ['native'] },
{ id: '4', symbol: 'ETH', name: 'Ether', chains: ['Stellar', 'Ethereum'], totalLiquidityUsd: 220000000, avgFeeUsd: 3.50, bridgeProviders: ['Wormhole', 'AllBridge'], tags: ['native'] },
{ id: '5', symbol: 'SOL', name: 'Solana', chains: ['Stellar', 'Solana'], totalLiquidityUsd: 67000000, avgFeeUsd: 1.20, bridgeProviders: ['Wormhole'], tags: ['native'] },
{ id: '6', symbol: 'MATIC', name: 'Polygon', chains: ['Stellar', 'Polygon'], totalLiquidityUsd: 31000000, avgFeeUsd: 0.60, bridgeProviders: ['Squid', 'Stargate'], tags: ['native'] },
{ id: '7', symbol: 'DAI', name: 'Dai', chains: ['Stellar', 'Ethereum', 'Polygon'], totalLiquidityUsd: 52000000, avgFeeUsd: 0.65, bridgeProviders: ['AllBridge', 'Squid'], tags: ['stablecoin'] },
{ id: '8', symbol: 'WBTC', name: 'Wrapped Bitcoin', chains: ['Stellar', 'Ethereum'], totalLiquidityUsd: 89000000, avgFeeUsd: 4.20, bridgeProviders: ['Wormhole'], tags: ['wrapped'] },
];

function formatUsd(value: number): string {
if (value >= 1_000_000_000) return `$${(value / 1_000_000_000).toFixed(1)}B`;
if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `$${(value / 1_000).toFixed(1)}K`;
return `$${value.toFixed(2)}`;
}

export default function StellarCrossChainAssetExplorer() {
const [assets] = useState<BridgeableAsset[]>(MOCK_ASSETS);
const [search, setSearch] = useState('');
const [chainFilter, setChainFilter] = useState('');

const filtered = useMemo(() => {
return assets.filter((a) => {
if (search && !a.symbol.toLowerCase().includes(search.toLowerCase()) && !a.name.toLowerCase().includes(search.toLowerCase())) return false;
if (chainFilter && !a.chains.includes(chainFilter)) return false;
return true;
});
}, [assets, search, chainFilter]);

const allChains = useMemo(() => Array.from(new Set(assets.flatMap((a) => a.chains))).sort(), [assets]);

return (
<div style={{ padding: 24, fontFamily: 'system-ui, sans-serif', maxWidth: 1100, margin: '0 auto' }}>
<h1 style={{ fontSize: 24, fontWeight: 700, color: '#0f172a', marginBottom: 4 }}>
Stellar Cross-Chain Asset Explorer
</h1>
<p style={{ color: '#64748b', marginBottom: 24 }}>
Discover bridgeable assets available across chains connected to Stellar.
</p>

<div style={{ display: 'flex', gap: 12, marginBottom: 24 }}>
<input
placeholder="Search by symbol or name..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ padding: '8px 12px', fontSize: 14, border: '1px solid #d1d5db', borderRadius: 6, flex: 1, maxWidth: 320 }}
/>
<select
style={{ padding: '8px 10px', border: '1px solid #e2e8f0', borderRadius: 6, fontSize: 13 }}
value={chainFilter}
onChange={(e) => setChainFilter(e.target.value)}
>
<option value="">All chains</option>
{allChains.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>

<div style={{ border: '1px solid #e2e8f0', borderRadius: 8, overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f8fafc' }}>
{['Asset', 'Available Chains', 'Total Liquidity', 'Avg Fee', 'Bridge Providers'].map((h) => (
<th key={h} style={{ padding: '10px 12px', textAlign: 'left', fontSize: 13, color: '#64748b', fontWeight: 600, whiteSpace: 'nowrap' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{filtered.length === 0 ? (
<tr>
<td colSpan={5} style={{ padding: 40, textAlign: 'center', color: '#94a3b8' }}>No assets match your search.</td>
</tr>
) : (
filtered.map((a) => (
<tr key={a.id} style={{ borderTop: '1px solid #e2e8f0' }}>
<td style={{ padding: '10px 12px' }}>
<div style={{ fontWeight: 700, fontSize: 15, color: '#0f172a' }}>{a.symbol}</div>
<div style={{ fontSize: 12, color: '#64748b' }}>{a.name}</div>
</td>
<td style={{ padding: '10px 12px' }}>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{a.chains.map((c) => (
<span key={c} style={{ padding: '2px 6px', background: '#f1f5f9', borderRadius: 4, fontSize: 11, color: '#475569' }}>{c}</span>
))}
</div>
</td>
<td style={{ padding: '10px 12px', fontWeight: 600, color: '#0f172a' }}>{formatUsd(a.totalLiquidityUsd)}</td>
<td style={{ padding: '10px 12px' }}>${a.avgFeeUsd.toFixed(2)}</td>
<td style={{ padding: '10px 12px', fontSize: 13, color: '#475569' }}>{a.bridgeProviders.join(', ')}</td>
</tr>
))
)}
</tbody>
</table>
</div>

<p style={{ marginTop: 16, fontSize: 12, color: '#94a3b8' }}>
Showing {filtered.length} of {assets.length} bridgeable assets.
</p>
</div>
);
}
Loading
Loading