diff --git a/apps/dashboard/providers/availability/components/Heatmap.tsx b/apps/dashboard/providers/availability/components/Heatmap.tsx new file mode 100644 index 00000000..a0d8a7df --- /dev/null +++ b/apps/dashboard/providers/availability/components/Heatmap.tsx @@ -0,0 +1,25 @@ + +import React from 'react'; +import { ProviderAvailability } from '../types'; +import { HeatmapRow } from './HeatmapRow'; + +export function Heatmap({ data, timeRange }: { data: ProviderAvailability[], timeRange: '7d' | '30d' | '90d' }) { + const compact = timeRange === '90d'; + const days = parseInt(timeRange.replace('d', ''), 10); + + return ( +
+
+
+
+
{data[0]?.slots[0]?.label}
+
+
{data[0]?.slots[days - 1]?.label}
+
+
Uptime
+
+ + {data.map(p => )} +
+ ); +} \ No newline at end of file diff --git a/apps/dashboard/providers/availability/components/HeatmapRow.tsx b/apps/dashboard/providers/availability/components/HeatmapRow.tsx new file mode 100644 index 00000000..79107233 --- /dev/null +++ b/apps/dashboard/providers/availability/components/HeatmapRow.tsx @@ -0,0 +1,59 @@ + +import React, { useState } from 'react'; +import { ProviderAvailability } from '../types'; +import { cellColor } from '../utils'; + +export function HeatmapRow({ data, compact }: { data: ProviderAvailability; compact: boolean }) { + const [hovered, setHovered] = useState(null); + const cellW = compact ? 8 : 14; + const cellGap = compact ? 2 : 3; + + return ( +
+
+ {data.provider} +
+ +
+ {data.slots.map((slot, i) => ( +
setHovered(i)} + onMouseLeave={() => setHovered(null)} + style={{ + width: `${cellW}px`, + height: '24px', + borderRadius: '3px', + backgroundColor: cellColor(slot.availability), + cursor: 'default', + flexShrink: 0, + position: 'relative', + }} + > + {hovered === i && ( +
+ {slot.label}: {(slot.availability * 100).toFixed(2)}% +
+ )} +
+ ))} +
+ +
+ {(data.overallAvailability * 100).toFixed(2)}% +
+
+ ); +} \ No newline at end of file diff --git a/apps/dashboard/providers/availability/data.ts b/apps/dashboard/providers/availability/data.ts new file mode 100644 index 00000000..9581f462 --- /dev/null +++ b/apps/dashboard/providers/availability/data.ts @@ -0,0 +1,34 @@ + +import { TimeRange, ProviderAvailability } from './types'; + +const PROVIDERS = ['AllBridge', 'Squid', 'Stargate']; + +function makeSlots(days: number, baseAvail: number, outageDays: number[]): any[] { + const now = new Date(); + return Array.from({ length: days }, (_, i) => { + const d = new Date(now); + d.setDate(d.getDate() - (days - 1 - i)); + const label = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + const outage = outageDays.includes(i); + const availability = outage ? Math.random() * 0.4 : baseAvail - Math.random() * 0.02; + return { label, availability: Math.max(0, Math.min(1, availability)), outage }; + }); +} + +export const DATA: Record = { + '7d': [ + { provider: 'AllBridge', slots: makeSlots(7, 0.998, []), overallAvailability: 0.998 }, + { provider: 'Squid', slots: makeSlots(7, 0.991, [4]), overallAvailability: 0.987 }, + { provider: 'Stargate', slots: makeSlots(7, 0.999, []), overallAvailability: 0.999 }, + ], + '30d': [ + { provider: 'AllBridge', slots: makeSlots(30, 0.997, [7, 21]), overallAvailability: 0.994 }, + { provider: 'Squid', slots: makeSlots(30, 0.988, [3, 14, 26]), overallAvailability: 0.981 }, + { provider: 'Stargate', slots: makeSlots(30, 0.999, [19]), overallAvailability: 0.996 }, + ], + '90d': [ + { provider: 'AllBridge', slots: makeSlots(90, 0.996, [12, 34, 67]), overallAvailability: 0.993 }, + { provider: 'Squid', slots: makeSlots(90, 0.985, [5, 22, 41, 78]), overallAvailability: 0.977 }, + { provider: 'Stargate', slots: makeSlots(90, 0.998, [55]), overallAvailability: 0.996 }, + ], +}; \ No newline at end of file diff --git a/apps/dashboard/providers/availability/page.tsx b/apps/dashboard/providers/availability/page.tsx index bf528871..3a4b82fb 100644 --- a/apps/dashboard/providers/availability/page.tsx +++ b/apps/dashboard/providers/availability/page.tsx @@ -1,227 +1,53 @@ -import React, { useState, useMemo } from 'react'; - -// ── Types ──────────────────────────────────────────────────────────────────── - -type TimeRange = '7d' | '30d' | '90d'; - -interface AvailabilitySlot { - label: string; - availability: number; // 0–1 - outage: boolean; -} - -interface ProviderAvailability { - provider: string; - slots: AvailabilitySlot[]; - overallAvailability: number; -} -// ── Mock data ───────────────────────────────────────────────────────────────── +'use client'; -const PROVIDERS = ['AllBridge', 'Squid', 'Stargate']; - -function makeSlots(days: number, baseAvail: number, outageDays: number[]): AvailabilitySlot[] { - const now = new Date(); - return Array.from({ length: days }, (_, i) => { - const d = new Date(now); - d.setDate(d.getDate() - (days - 1 - i)); - const label = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - const outage = outageDays.includes(i); - const availability = outage ? Math.random() * 0.4 : baseAvail - Math.random() * 0.02; - return { label, availability: Math.max(0, Math.min(1, availability)), outage }; - }); -} - -const DATA: Record = { - '7d': [ - { provider: 'AllBridge', slots: makeSlots(7, 0.998, []), overallAvailability: 0.998 }, - { provider: 'Squid', slots: makeSlots(7, 0.991, [4]), overallAvailability: 0.987 }, - { provider: 'Stargate', slots: makeSlots(7, 0.999, []), overallAvailability: 0.999 }, - ], - '30d': [ - { provider: 'AllBridge', slots: makeSlots(30, 0.997, [7, 21]), overallAvailability: 0.994 }, - { provider: 'Squid', slots: makeSlots(30, 0.988, [3, 14, 26]), overallAvailability: 0.981 }, - { provider: 'Stargate', slots: makeSlots(30, 0.999, [19]), overallAvailability: 0.996 }, - ], - '90d': [ - { provider: 'AllBridge', slots: makeSlots(90, 0.996, [12, 34, 67]), overallAvailability: 0.993 }, - { provider: 'Squid', slots: makeSlots(90, 0.985, [5, 22, 41, 78]), overallAvailability: 0.977 }, - { provider: 'Stargate', slots: makeSlots(90, 0.998, [55]), overallAvailability: 0.996 }, - ], -}; - -// ── Heatmap cell ────────────────────────────────────────────────────────────── +import React, { useState, useMemo } from 'react'; +import { DATA } from './data'; +import { TimeRange } from './types'; +import { Heatmap } from './components/Heatmap'; -function cellColor(availability: number): string { - if (availability >= 0.999) return '#16a34a'; - if (availability >= 0.995) return '#4ade80'; - if (availability >= 0.98) return '#fbbf24'; - if (availability >= 0.95) return '#f97316'; - return '#ef4444'; -} +// ── Main component ─────────────────────────────────────────────────────────── -function HeatmapRow({ data, compact }: { data: ProviderAvailability; compact: boolean }) { - const [hovered, setHovered] = useState(null); - const cellW = compact ? 8 : 14; - const cellGap = compact ? 2 : 3; +export default function ProviderAvailabilityPage() { + const [timeRange, setTimeRange] = useState('30d'); + const data = useMemo(() => DATA[timeRange], [timeRange]); return ( -
-
- {data.provider} -
- -
- {data.slots.map((slot, i) => ( -
setHovered(i)} - onMouseLeave={() => setHovered(null)} - style={{ - width: `${cellW}px`, - height: '24px', - borderRadius: '3px', - backgroundColor: cellColor(slot.availability), - cursor: 'default', - flexShrink: 0, - position: 'relative', - }} - > - {hovered === i && ( -
- {slot.label}: {(slot.availability * 100).toFixed(2)}% - {slot.outage && ' ⚠ outage'} -
- )} +
+
+
+
+

Provider Availability

+

Uptime and outage history for connected providers.

- ))} -
- -
- {(data.overallAvailability * 100).toFixed(2)}% -
-
- ); -} - -// ── Legend ──────────────────────────────────────────────────────────────────── -function Legend() { - const items = [ - { color: '#16a34a', label: '≥ 99.9%' }, - { color: '#4ade80', label: '≥ 99.5%' }, - { color: '#fbbf24', label: '≥ 98%' }, - { color: '#f97316', label: '≥ 95%' }, - { color: '#ef4444', label: '< 95%' }, - ]; - return ( -
- {items.map(({ color, label }) => ( -
-
- {label} -
- ))} -
- ); -} - -// ── Main page ───────────────────────────────────────────────────────────────── - -export default function ProviderAvailabilityHeatmap() { - const [range, setRange] = useState('30d'); - const [filter, setFilter] = useState('All'); - - const providers = useMemo(() => { - const rows = DATA[range]; - return filter === 'All' ? rows : rows.filter((r) => r.provider === filter); - }, [range, filter]); - - const compact = range === '90d'; - - return ( -
-

- Stellar Provider Availability Heatmap -

-

- Visualize provider uptime trends and outage periods over time. -

- - {/* Controls */} -
-
- {(['7d', '30d', '90d'] as TimeRange[]).map((r) => ( - - ))} -
- - -
- - {/* Heatmap */} -
-
- - Availability — last {range} - - -
- - {providers.map((p) => ( - - ))} -
+
+ {(['7d', '30d', '90d'] as TimeRange[]).map(range => ( + + ))} +
+ - {/* Outage summary */} -
- Outage periods - - Orange/red cells indicate degraded or unavailable service. Hover for details. - +
+ +
); -} +} \ No newline at end of file diff --git a/apps/dashboard/providers/availability/types.ts b/apps/dashboard/providers/availability/types.ts new file mode 100644 index 00000000..eb0c66f8 --- /dev/null +++ b/apps/dashboard/providers/availability/types.ts @@ -0,0 +1,14 @@ + +export type TimeRange = '7d' | '30d' | '90d'; + +export interface AvailabilitySlot { + label: string; + availability: number; // 0–1 + outage: boolean; +} + +export interface ProviderAvailability { + provider: string; + slots: AvailabilitySlot[]; + overallAvailability: number; +} \ No newline at end of file diff --git a/apps/dashboard/providers/availability/utils.ts b/apps/dashboard/providers/availability/utils.ts new file mode 100644 index 00000000..cc16bfed --- /dev/null +++ b/apps/dashboard/providers/availability/utils.ts @@ -0,0 +1,8 @@ + +export function cellColor(availability: number): string { + if (availability >= 0.999) return '#16a34a'; + if (availability >= 0.995) return '#4ade80'; + if (availability >= 0.98) return '#fbbf24'; + if (availability >= 0.95) return '#f97316'; + return '#ef4444'; +} \ No newline at end of file diff --git a/src/jobs/assets/sync/stellar/fetcher.ts b/src/jobs/assets/sync/stellar/fetcher.ts new file mode 100644 index 00000000..fc71e5c5 --- /dev/null +++ b/src/jobs/assets/sync/stellar/fetcher.ts @@ -0,0 +1,11 @@ + +import { Asset } from './types'; + +// Mock implementation of a fetcher for Stellar assets +export async function fetchLatestAssets(): Promise { + // In a real implementation, this would fetch data from the Stellar network + return Promise.resolve([ + { id: '1', code: 'USD', issuer: 'native' }, + { id: '2', code: 'EUR', issuer: 'native' }, + ]); +} \ No newline at end of file diff --git a/src/jobs/assets/sync/stellar/registry.ts b/src/jobs/assets/sync/stellar/registry.ts new file mode 100644 index 00000000..58344138 --- /dev/null +++ b/src/jobs/assets/sync/stellar/registry.ts @@ -0,0 +1,14 @@ + +import { Registry } from './types'; + +// Mock implementation of a registry +let registry: Registry = { assets: [] }; + +export async function getRegistry(): Promise { + return Promise.resolve(registry); +} + +export async function updateRegistry(updatedRegistry: Registry): Promise { + registry = updatedRegistry; + return Promise.resolve(); +} \ No newline at end of file diff --git a/src/jobs/assets/sync/stellar/scheduler.ts b/src/jobs/assets/sync/stellar/scheduler.ts new file mode 100644 index 00000000..a6f25a60 --- /dev/null +++ b/src/jobs/assets/sync/stellar/scheduler.ts @@ -0,0 +1,14 @@ + +import { synchronize } from './synchronizer'; + +const SYNC_INTERVAL = 1000 * 60 * 60; // 1 hour + +export function startScheduler(): void { + console.log('Starting asset registry synchronization scheduler...'); + + // Run the synchronization job immediately + synchronize(); + + // Schedule the job to run at a regular interval + setInterval(synchronize, SYNC_INTERVAL); +} \ No newline at end of file diff --git a/src/jobs/assets/sync/stellar/synchronizer.ts b/src/jobs/assets/sync/stellar/synchronizer.ts new file mode 100644 index 00000000..88329990 --- /dev/null +++ b/src/jobs/assets/sync/stellar/synchronizer.ts @@ -0,0 +1,30 @@ + +import { getRegistry, updateRegistry } from './registry'; +import { fetchLatestAssets } from './fetcher'; +import { Asset } from './types'; + +export async function synchronize(): Promise { + console.log('Starting asset registry synchronization...'); + + const currentRegistry = await getRegistry(); + const latestAssets = await fetchLatestAssets(); + + const currentAssetIds = new Set(currentRegistry.assets.map((asset) => asset.id)); + const latestAssetIds = new Set(latestAssets.map((asset) => asset.id)); + + const newAssets = latestAssets.filter((asset) => !currentAssetIds.has(asset.id)); + const removedAssets = currentRegistry.assets.filter((asset) => !latestAssetIds.has(asset.id)); + + if (newAssets.length > 0 || removedAssets.length > 0) { + console.log('Changes detected. Updating registry...'); + console.log('New assets:', newAssets); + console.log('Removed assets:', removedAssets); + + await updateRegistry({ assets: latestAssets }); + console.log('Registry updated successfully.'); + } else { + console.log('No changes detected. Registry is up to date.'); + } + + console.log('Asset registry synchronization finished.'); +} \ No newline at end of file diff --git a/src/jobs/assets/sync/stellar/types.ts b/src/jobs/assets/sync/stellar/types.ts new file mode 100644 index 00000000..afb22072 --- /dev/null +++ b/src/jobs/assets/sync/stellar/types.ts @@ -0,0 +1,10 @@ + +export interface Asset { + id: string; + code: string; + issuer: string; +} + +export interface Registry { + assets: Asset[]; +} \ No newline at end of file diff --git a/src/rules/routes/stellar/engine.ts b/src/rules/routes/stellar/engine.ts new file mode 100644 index 00000000..2f9802ce --- /dev/null +++ b/src/rules/routes/stellar/engine.ts @@ -0,0 +1,21 @@ + +import { SorobanRouteRule, SorobanRouteEligibility } from './types'; + +export class SorobanRouteEligibilityEngine { + constructor(private rules: SorobanRouteRule[]) {} + + async check(route: any): Promise { + const unsatisfiedRules: string[] = []; + + for (const rule of this.rules) { + if (!(await rule.isEligible(route))) { + unsatisfiedRules.push(rule.name); + } + } + + return { + isEligible: unsatisfiedRules.length === 0, + unsatisfiedRules, + }; + } +} \ No newline at end of file diff --git a/src/rules/routes/stellar/index.ts b/src/rules/routes/stellar/index.ts index 0240a32a..1e9bb823 100644 --- a/src/rules/routes/stellar/index.ts +++ b/src/rules/routes/stellar/index.ts @@ -1,2 +1,4 @@ + export * from './types'; -export * from './route-eligibility-rules'; +export * from './registry'; +export * from './engine'; \ No newline at end of file diff --git a/src/rules/routes/stellar/registry.ts b/src/rules/routes/stellar/registry.ts new file mode 100644 index 00000000..0d4af13a --- /dev/null +++ b/src/rules/routes/stellar/registry.ts @@ -0,0 +1,14 @@ + +import { SorobanRouteRule } from './types'; + +export class SorobanRouteRuleRegistry { + private rules: SorobanRouteRule[] = []; + + register(rule: SorobanRouteRule) { + this.rules.push(rule); + } + + getAll(): SorobanRouteRule[] { + return this.rules; + } +} \ No newline at end of file diff --git a/src/rules/routes/stellar/types.ts b/src/rules/routes/stellar/types.ts index 6c9ec520..75c0197c 100644 --- a/src/rules/routes/stellar/types.ts +++ b/src/rules/routes/stellar/types.ts @@ -1,42 +1,11 @@ -export type RuleOperator = 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'in' | 'not_in'; -export interface RuleCondition { - field: string; - operator: RuleOperator; - value: unknown; -} - -export interface RouteRule { - id: string; +export interface SorobanRouteRule { name: string; description: string; - conditions: RuleCondition[]; - action: 'allow' | 'deny'; - priority: number; - enabled: boolean; - metadata?: Record; -} - -export interface RouteContext { - sourceChain: string; - destinationChain: string; - asset: string; - amount: string; - senderAddress?: string; - recipientAddress?: string; - attributes?: Record; + isEligible(route: any): Promise; } -export interface EligibilityResult { - eligible: boolean; - appliedRule?: RouteRule; - reason: string; - evaluatedRules: number; -} - -export interface RuleEvaluationLog { - ruleId: string; - ruleName: string; - matched: boolean; - action: 'allow' | 'deny'; -} +export interface SorobanRouteEligibility { + isEligible: boolean; + unsatisfiedRules: string[]; +} \ No newline at end of file