diff --git a/apps/dashboard/recovery/stellar/page.tsx b/apps/dashboard/recovery/stellar/page.tsx new file mode 100644 index 00000000..3ce8c4a8 --- /dev/null +++ b/apps/dashboard/recovery/stellar/page.tsx @@ -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 = { + pending: '#f59e0b', + in_progress: '#3b82f6', + recovered: '#22c55e', + failed: '#ef4444', +}; + +function StatusBadge({ status }: { status: string }) { + const color = STATUS_COLORS[status] || '#64748b'; + return ( + + + {status.replace('_', ' ')} + + ); +} + +function SummaryCard({ label, value, color }: { label: string; value: number; color: string }) { + return ( +
+
{value}
+
{label}
+
+ ); +} + +export default function StellarTransferRecoveryDashboard() { + const [transfers] = useState(MOCK_FAILED_TRANSFERS); + const [filter, setFilter] = useState({}); + + 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 ( +
+

+ Stellar Transfer Recovery Dashboard +

+

+ Monitor and manage failed transfer recovery operations. +

+ +
+ + + + + +
+ +
+ + + + +
+ +
+
+

Failed Transfers

+
+ {filtered.length === 0 ? ( +
No transfers match the filters.
+ ) : ( + + + + {['Transfer Hash', 'Route', 'Asset', 'Amount', 'Failure Reason', 'Status', 'Retries'].map((h) => ( + + ))} + + + + {filtered.map((t) => ( + + + + + + + + + + ))} + +
{h}
{t.transferHash.slice(0, 12)}...{t.sourceChain} → {t.destinationChain}{t.asset}{t.amount}{t.failureReason}{t.retryCount}
+ )} +
+
+ ); +} diff --git a/apps/dashboard/recovery/stellar/types.ts b/apps/dashboard/recovery/stellar/types.ts new file mode 100644 index 00000000..cb78233a --- /dev/null +++ b/apps/dashboard/recovery/stellar/types.ts @@ -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; +} diff --git a/apps/explorer/assets/stellar/page.tsx b/apps/explorer/assets/stellar/page.tsx new file mode 100644 index 00000000..2a5109ce --- /dev/null +++ b/apps/explorer/assets/stellar/page.tsx @@ -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(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 ( +
+

+ Stellar Cross-Chain Asset Explorer +

+

+ Discover bridgeable assets available across chains connected to Stellar. +

+ +
+ setSearch(e.target.value)} + style={{ padding: '8px 12px', fontSize: 14, border: '1px solid #d1d5db', borderRadius: 6, flex: 1, maxWidth: 320 }} + /> + +
+ +
+ + + + {['Asset', 'Available Chains', 'Total Liquidity', 'Avg Fee', 'Bridge Providers'].map((h) => ( + + ))} + + + + {filtered.length === 0 ? ( + + + + ) : ( + filtered.map((a) => ( + + + + + + + + )) + )} + +
{h}
No assets match your search.
+
{a.symbol}
+
{a.name}
+
+
+ {a.chains.map((c) => ( + {c} + ))} +
+
{formatUsd(a.totalLiquidityUsd)}${a.avgFeeUsd.toFixed(2)}{a.bridgeProviders.join(', ')}
+
+ +

+ Showing {filtered.length} of {assets.length} bridgeable assets. +

+
+ ); +} diff --git a/src/integrations/registry/stellar/bridge-integration-registry.ts b/src/integrations/registry/stellar/bridge-integration-registry.ts new file mode 100644 index 00000000..cd203750 --- /dev/null +++ b/src/integrations/registry/stellar/bridge-integration-registry.ts @@ -0,0 +1,160 @@ +import { + BridgeIntegration, + IntegrationDiscoveryRequest, + IntegrationRegistryStats, +} from './bridge-integration-registry.types'; + +export class BridgeIntegrationNotFoundError extends Error { + constructor(id: string) { + super(`Bridge integration not found: "${id}"`); + this.name = 'BridgeIntegrationNotFoundError'; + } +} + +export class BridgeIntegrationRegistrationError extends Error { + constructor(message: string) { + super(message); + this.name = 'BridgeIntegrationRegistrationError'; + } +} + +export class BridgeIntegrationRegistry { + private readonly integrations: Map = new Map(); + + register(integration: BridgeIntegration): void { + this.validate(integration); + const key = integration.id.toLowerCase(); + this.integrations.set(key, { ...integration, id: key }); + } + + registerBatch(integrations: BridgeIntegration[]): void { + for (const integration of integrations) this.validate(integration); + for (const integration of integrations) this.register(integration); + } + + deregister(id: string): boolean { + return this.integrations.delete(id.toLowerCase()); + } + + get(id: string): BridgeIntegration | undefined { + return this.integrations.get(id.toLowerCase()); + } + + getOrThrow(id: string): BridgeIntegration { + const integration = this.get(id); + if (!integration) throw new BridgeIntegrationNotFoundError(id); + return integration; + } + + getAll(): BridgeIntegration[] { + return Array.from(this.integrations.values()); + } + + discover(request: IntegrationDiscoveryRequest): BridgeIntegration[] { + return this.getAll().filter((i) => { + if (request.chain && !i.chains.includes(request.chain)) return false; + if (request.asset && !i.assets.includes(request.asset)) return false; + if (request.status && i.status !== request.status) return false; + return true; + }); + } + + getByProvider(provider: string): BridgeIntegration[] { + return this.getAll().filter( + (i) => i.provider.toLowerCase() === provider.toLowerCase(), + ); + } + + stats(): IntegrationRegistryStats { + const all = this.getAll(); + const chains = new Set(); + const providers = new Set(); + for (const i of all) { + for (const c of i.chains) chains.add(c); + providers.add(i.provider); + } + return { + totalIntegrations: all.length, + activeIntegrations: all.filter((i) => i.status === 'active').length, + supportedChains: [...chains].sort(), + providers: [...providers].sort(), + }; + } + + private validate(integration: BridgeIntegration): void { + if (!integration.id?.trim()) { + throw new BridgeIntegrationRegistrationError( + 'Integration id must be a non-empty string', + ); + } + if (!integration.name?.trim()) { + throw new BridgeIntegrationRegistrationError( + `Integration "${integration.id}": name must be a non-empty string`, + ); + } + if (!integration.provider?.trim()) { + throw new BridgeIntegrationRegistrationError( + `Integration "${integration.id}": provider must be a non-empty string`, + ); + } + if (!Array.isArray(integration.chains) || integration.chains.length === 0) { + throw new BridgeIntegrationRegistrationError( + `Integration "${integration.id}": must support at least one chain`, + ); + } + if (!Array.isArray(integration.assets) || integration.assets.length === 0) { + throw new BridgeIntegrationRegistrationError( + `Integration "${integration.id}": must support at least one asset`, + ); + } + } +} + +export const defaultBridgeIntegrationRegistry = new BridgeIntegrationRegistry(); + +defaultBridgeIntegrationRegistry.registerBatch([ + { + id: 'allbridge-stellar-eth', + name: 'AllBridge Stellar-Ethereum', + provider: 'AllBridge', + version: '2.1.0', + chains: ['Stellar', 'Ethereum'], + assets: ['USDC', 'USDT', 'ETH'], + status: 'active', + metadata: { docs: 'https://docs.allbridge.io' }, + registeredAt: new Date().toISOString(), + }, + { + id: 'squid-stellar-polygon', + name: 'Squid Router Stellar-Polygon', + provider: 'Squid', + version: '1.4.2', + chains: ['Stellar', 'Polygon'], + assets: ['USDC', 'USDT'], + status: 'active', + metadata: { docs: 'https://docs.squidrouter.com' }, + registeredAt: new Date().toISOString(), + }, + { + id: 'stargate-stellar-base', + name: 'Stargate Stellar-Base', + provider: 'Stargate', + version: '1.0.0', + chains: ['Stellar', 'Base'], + assets: ['USDC', 'XLM'], + status: 'beta', + metadata: { docs: 'https://stargate.finance' }, + registeredAt: new Date().toISOString(), + }, + { + id: 'wormhole-stellar-eth', + name: 'Wormhole Stellar-Ethereum', + provider: 'Wormhole', + version: '3.0.1', + chains: ['Stellar', 'Ethereum', 'Solana'], + assets: ['USDC', 'SOL', 'ETH'], + status: 'active', + metadata: { docs: 'https://docs.wormhole.com' }, + registeredAt: new Date().toISOString(), + }, +]); diff --git a/src/integrations/registry/stellar/bridge-integration-registry.types.ts b/src/integrations/registry/stellar/bridge-integration-registry.types.ts new file mode 100644 index 00000000..4cd8e05e --- /dev/null +++ b/src/integrations/registry/stellar/bridge-integration-registry.types.ts @@ -0,0 +1,24 @@ +export interface BridgeIntegration { + id: string; + name: string; + provider: string; + version: string; + chains: string[]; + assets: string[]; + status: 'active' | 'deprecated' | 'beta'; + metadata: Record; + registeredAt: string; +} + +export interface IntegrationDiscoveryRequest { + chain?: string; + asset?: string; + status?: 'active' | 'deprecated' | 'beta'; +} + +export interface IntegrationRegistryStats { + totalIntegrations: number; + activeIntegrations: number; + supportedChains: string[]; + providers: string[]; +} diff --git a/src/integrations/registry/stellar/index.ts b/src/integrations/registry/stellar/index.ts new file mode 100644 index 00000000..23ae79f0 --- /dev/null +++ b/src/integrations/registry/stellar/index.ts @@ -0,0 +1,2 @@ +export { BridgeIntegrationRegistry, BridgeIntegrationNotFoundError, BridgeIntegrationRegistrationError, defaultBridgeIntegrationRegistry } from './bridge-integration-registry'; +export type { BridgeIntegration, IntegrationDiscoveryRequest, IntegrationRegistryStats } from './bridge-integration-registry.types'; diff --git a/tests/datasets/routes/stellar/route-benchmark-dataset.spec.ts b/tests/datasets/routes/stellar/route-benchmark-dataset.spec.ts new file mode 100644 index 00000000..9d861b4e --- /dev/null +++ b/tests/datasets/routes/stellar/route-benchmark-dataset.spec.ts @@ -0,0 +1,175 @@ +interface BenchmarkScenario { + id: string; + name: string; + sourceChain: string; + destChain: string; + asset: string; + amount: string; + providers: string[]; + expectedLatencyMs: number; + expectedFeeUsd: number; + tags: string[]; +} + +interface BenchmarkDataset { + name: string; + description: string; + version: string; + scenarios: BenchmarkScenario[]; +} + +const STELLAR_ROUTE_BENCHMARK_DATASET: BenchmarkDataset = { + name: 'Stellar Route Benchmark Dataset', + description: 'Standardized benchmark scenarios for evaluating Soroban route recommendation algorithms across multiple providers and chains.', + version: '1.0.0', + scenarios: [ + { + id: 'stellar-eth-usdc-large', + name: 'Large USDC transfer Stellar to Ethereum', + sourceChain: 'Stellar', + destChain: 'Ethereum', + asset: 'USDC', + amount: '100000.00', + providers: ['AllBridge', 'Squid', 'Wormhole'], + expectedLatencyMs: 4200, + expectedFeeUsd: 1.50, + tags: ['large', 'stablecoin', 'high-value'], + }, + { + id: 'stellar-polygon-usdc-small', + name: 'Small USDC transfer Stellar to Polygon', + sourceChain: 'Stellar', + destChain: 'Polygon', + asset: 'USDC', + amount: '50.00', + providers: ['AllBridge', 'Stargate'], + expectedLatencyMs: 3100, + expectedFeeUsd: 0.80, + tags: ['small', 'stablecoin', 'low-value'], + }, + { + id: 'stellar-base-xlm', + name: 'XLM transfer Stellar to Base', + sourceChain: 'Stellar', + destChain: 'Base', + asset: 'XLM', + amount: '10000.00', + providers: ['AllBridge'], + expectedLatencyMs: 2800, + expectedFeeUsd: 0.30, + tags: ['native', 'medium-value'], + }, + { + id: 'eth-stellar-usdc', + name: 'USDC transfer Ethereum to Stellar', + sourceChain: 'Ethereum', + destChain: 'Stellar', + asset: 'USDC', + amount: '25000.00', + providers: ['Squid', 'Wormhole'], + expectedLatencyMs: 6700, + expectedFeeUsd: 2.10, + tags: ['stablecoin', 'high-value', 'eth'], + }, + { + id: 'polygon-stellar-usdt', + name: 'USDT transfer Polygon to Stellar', + sourceChain: 'Polygon', + destChain: 'Stellar', + asset: 'USDT', + amount: '5000.00', + providers: ['Stargate'], + expectedLatencyMs: 3500, + expectedFeeUsd: 0.60, + tags: ['stablecoin', 'medium-value'], + }, + { + id: 'stellar-solana-usdc', + name: 'USDC transfer Stellar to Solana', + sourceChain: 'Stellar', + destChain: 'Solana', + asset: 'USDC', + amount: '15000.00', + providers: ['Wormhole'], + expectedLatencyMs: 5100, + expectedFeeUsd: 1.20, + tags: ['stablecoin', 'high-value', 'solana'], + }, + { + id: 'stellar-eth-eth-small', + name: 'Small ETH transfer Stellar to Ethereum', + sourceChain: 'Stellar', + destChain: 'Ethereum', + asset: 'ETH', + amount: '1.50', + providers: ['AllBridge', 'Wormhole'], + expectedLatencyMs: 4800, + expectedFeeUsd: 3.50, + tags: ['native', 'low-value', 'eth'], + }, + { + id: 'base-stellar-usdc', + name: 'USDC transfer Base to Stellar', + sourceChain: 'Base', + destChain: 'Stellar', + asset: 'USDC', + amount: '8000.00', + providers: ['AllBridge'], + expectedLatencyMs: 3200, + expectedFeeUsd: 0.90, + tags: ['stablecoin', 'medium-value', 'l2'], + }, + ], +}; + +function validateDataset(dataset: BenchmarkDataset): string[] { + const errors: string[] = []; + const ids = new Set(); + + for (const scenario of dataset.scenarios) { + if (!scenario.id.trim()) errors.push('Scenario has empty id'); + if (ids.has(scenario.id)) errors.push(`Duplicate scenario id: ${scenario.id}`); + ids.add(scenario.id); + + if (!scenario.name.trim()) errors.push(`Scenario "${scenario.id}" has empty name`); + if (!scenario.sourceChain.trim()) errors.push(`Scenario "${scenario.id}" has empty sourceChain`); + if (!scenario.destChain.trim()) errors.push(`Scenario "${scenario.id}" has empty destChain`); + if (!scenario.asset.trim()) errors.push(`Scenario "${scenario.id}" has empty asset`); + if (!scenario.amount.trim()) errors.push(`Scenario "${scenario.id}" has empty amount`); + if (!scenario.providers || scenario.providers.length === 0) errors.push(`Scenario "${scenario.id}" has no providers`); + if (scenario.expectedLatencyMs < 0) errors.push(`Scenario "${scenario.id}" has negative expectedLatencyMs`); + if (scenario.expectedFeeUsd < 0) errors.push(`Scenario "${scenario.id}" has negative expectedFeeUsd`); + } + + return errors; +} + +function countByTag(dataset: BenchmarkDataset): Record { + const counts: Record = {}; + for (const scenario of dataset.scenarios) { + for (const tag of scenario.tags) { + counts[tag] = (counts[tag] || 0) + 1; + } + } + return counts; +} + +const validationErrors = validateDataset(STELLAR_ROUTE_BENCHMARK_DATASET); +const tagCounts = countByTag(STELLAR_ROUTE_BENCHMARK_DATASET); + +function printDatasetReport(): void { + console.log(`Dataset: ${STELLAR_ROUTE_BENCHMARK_DATASET.name} v${STELLAR_ROUTE_BENCHMARK_DATASET.version}`); + console.log(`Description: ${STELLAR_ROUTE_BENCHMARK_DATASET.description}`); + console.log(`Total scenarios: ${STELLAR_ROUTE_BENCHMARK_DATASET.scenarios.length}`); + console.log(`Validation errors: ${validationErrors.length === 0 ? 'None' : validationErrors.join(', ')}`); + console.log('\nTag distribution:'); + for (const [tag, count] of Object.entries(tagCounts).sort()) { + console.log(` ${tag}: ${count} scenarios`); + } + console.log('\nScenarios:'); + for (const s of STELLAR_ROUTE_BENCHMARK_DATASET.scenarios) { + console.log(` [${s.id}] ${s.sourceChain} -> ${s.destChain} | ${s.amount} ${s.asset} | providers: ${s.providers.join(', ')}`); + } +} + +printDatasetReport();