diff --git a/src/monitoring/sla/providers/stellar/index.ts b/src/monitoring/sla/providers/stellar/index.ts new file mode 100644 index 00000000..a8d6c0de --- /dev/null +++ b/src/monitoring/sla/providers/stellar/index.ts @@ -0,0 +1,22 @@ +/** + * Stellar Provider SLA Evaluator Module + * + * @see Issue #469 — Implement Stellar Provider SLA Evaluator + */ + +export { StellarProviderSlaEvaluator } from './stellar-provider-sla-evaluator'; + +export type { + ProviderId, + SLATargets, + UptimeCheck, + UptimeWindow, + ResponseTimeStats, + ComplianceVerdict, + SLABreakdown, + SLAEvaluation, + SLAProviderReport, + SLAEvaluatorConfig, +} from './types'; + +export { DEFAULT_SLA_TARGETS } from './types'; diff --git a/src/monitoring/sla/providers/stellar/stellar-provider-sla-evaluator.ts b/src/monitoring/sla/providers/stellar/stellar-provider-sla-evaluator.ts new file mode 100644 index 00000000..e1619cc3 --- /dev/null +++ b/src/monitoring/sla/providers/stellar/stellar-provider-sla-evaluator.ts @@ -0,0 +1,320 @@ +/** + * Stellar Provider SLA Evaluator + * + * Evaluates bridge providers against configurable SLA targets by tracking + * uptime checks, measuring response times, and generating structured reports. + * + * @see Issue #469 — Implement Stellar Provider SLA Evaluator + */ + +import type { + ComplianceVerdict, + ProviderId, + SLABreakdown, + SLAEvaluation, + SLAEvaluatorConfig, + SLAProviderReport, + SLATargets, + UptimeCheck, + UptimeWindow, + ResponseTimeStats, +} from './types'; + +import { DEFAULT_SLA_TARGETS } from './types'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function percentile(sorted: number[], pct: number): number { + if (sorted.length === 0) return 0; + const idx = Math.ceil((pct / 100) * sorted.length) - 1; + return sorted[Math.max(0, Math.min(idx, sorted.length - 1))]; +} + +function verdict(actual: number, target: number, higherIsBetter: boolean): ComplianceVerdict { + const ratio = actual / target; + if (higherIsBetter) { + if (actual >= target) return 'pass'; + if (ratio >= 0.97) return 'warn'; + return 'fail'; + } else { + if (actual <= target) return 'pass'; + if (ratio <= 1.1) return 'warn'; + return 'fail'; + } +} + +function worstVerdict(verdicts: ComplianceVerdict[]): ComplianceVerdict { + if (verdicts.includes('fail')) return 'fail'; + if (verdicts.includes('warn')) return 'warn'; + return 'pass'; +} + +function computeResponseStats(checks: UptimeCheck[]): ResponseTimeStats { + const times = checks + .map((c) => c.responseMs) + .filter((ms): ms is number => ms !== undefined) + .sort((a, b) => a - b); + + if (times.length === 0) { + return { avgMs: 0, p50Ms: 0, p95Ms: 0, p99Ms: 0, minMs: 0, maxMs: 0 }; + } + + const avg = times.reduce((a, b) => a + b, 0) / times.length; + return { + avgMs: Math.round(avg), + p50Ms: percentile(times, 50), + p95Ms: percentile(times, 95), + p99Ms: percentile(times, 99), + minMs: times[0], + maxMs: times[times.length - 1], + }; +} + +function computeUptimeWindow(checks: UptimeCheck[], label: string, durationMs: number): UptimeWindow { + const total = checks.length; + const successful = checks.filter((c) => c.available).length; + return { + label, + durationMs, + totalChecks: total, + successfulChecks: successful, + uptimePct: total === 0 ? 100 : (successful / total) * 100, + }; +} + +function buildRecommendations(breakdown: SLABreakdown): string[] { + const recs: string[] = []; + if (breakdown.uptime.verdict !== 'pass') { + recs.push( + `Uptime is ${breakdown.uptime.actual.toFixed(2)}% — below target of ${breakdown.uptime.target}%. Investigate provider availability and add redundant endpoints.`, + ); + } + if (breakdown.avgResponse.verdict !== 'pass') { + recs.push( + `Average response time is ${breakdown.avgResponse.actual}ms — exceeds target of ${breakdown.avgResponse.target}ms. Review provider network latency and consider geographically closer nodes.`, + ); + } + if (breakdown.p99Response.verdict !== 'pass') { + recs.push( + `P99 response time is ${breakdown.p99Response.actual}ms — exceeds target of ${breakdown.p99Response.target}ms. Investigate tail-latency causes such as GC pauses or contention.`, + ); + } + if (breakdown.successRate.verdict !== 'pass') { + recs.push( + `Success rate is ${(breakdown.successRate.actual * 100).toFixed(1)}% — below target of ${(breakdown.successRate.target * 100).toFixed(1)}%. Review error logs for common failure patterns.`, + ); + } + if (recs.length === 0) { + recs.push('All SLA targets are currently met. Continue monitoring.'); + } + return recs; +} + +// ─── Evaluator ──────────────────────────────────────────────────────────────── + +export class StellarProviderSlaEvaluator { + private readonly checks = new Map(); + private readonly targets: SLATargets; + private readonly windowMs: number; + private readonly prevOverall = new Map(); + + private readonly onBreach?: SLAEvaluatorConfig['onBreach']; + private readonly onRecovery?: SLAEvaluatorConfig['onRecovery']; + + constructor(config: SLAEvaluatorConfig = {}) { + this.windowMs = config.windowMs ?? 86_400_000; + this.targets = { ...DEFAULT_SLA_TARGETS, ...config.targets }; + this.onBreach = config.onBreach; + this.onRecovery = config.onRecovery; + } + + // ─── Uptime Recording ────────────────────────────────────────────────────── + + /** + * Record the result of a single probe call for a provider. + */ + recordCheck(providerId: ProviderId, check: UptimeCheck): void { + if (!this.checks.has(providerId)) { + this.checks.set(providerId, []); + } + this.checks.get(providerId)!.push(check); + this.pruneOld(providerId); + } + + /** + * Convenience method to record an available probe. + */ + recordSuccess(providerId: ProviderId, responseMs: number): void { + this.recordCheck(providerId, { timestamp: new Date(), available: true, responseMs }); + } + + /** + * Convenience method to record a failed probe. + */ + recordFailure(providerId: ProviderId, error: string): void { + this.recordCheck(providerId, { timestamp: new Date(), available: false, error }); + } + + // ─── Evaluation ──────────────────────────────────────────────────────────── + + /** + * Evaluate a provider's SLA compliance over the configured window. + */ + evaluate(providerId: ProviderId): SLAEvaluation { + const checks = this.getWindowChecks(providerId); + const uptime = computeUptimeWindow(checks, `${this.windowMs / 3_600_000}h`, this.windowMs); + const responseTime = computeResponseStats(checks); + const total = checks.length; + const successes = checks.filter((c) => c.available).length; + const successRate = total === 0 ? 1 : successes / total; + + const breakdown: SLABreakdown = { + uptime: { + verdict: verdict(uptime.uptimePct, this.targets.minUptimePct, true), + actual: uptime.uptimePct, + target: this.targets.minUptimePct, + }, + avgResponse: { + verdict: verdict(responseTime.avgMs, this.targets.maxAvgResponseMs, false), + actual: responseTime.avgMs, + target: this.targets.maxAvgResponseMs, + }, + p99Response: { + verdict: verdict(responseTime.p99Ms, this.targets.maxP99ResponseMs, false), + actual: responseTime.p99Ms, + target: this.targets.maxP99ResponseMs, + }, + successRate: { + verdict: verdict(successRate, this.targets.minSuccessRate, true), + actual: successRate, + target: this.targets.minSuccessRate, + }, + }; + + const overall = worstVerdict( + Object.values(breakdown).map((b) => b.verdict), + ); + + const evaluation: SLAEvaluation = { + providerId, + evaluatedAt: new Date(), + windowMs: this.windowMs, + targets: this.targets, + uptime, + responseTime, + successRate, + breakdown, + overall, + }; + + this.handleTransitions(providerId, overall, evaluation); + return evaluation; + } + + /** + * Evaluate all tracked providers and return sorted results (worst first). + */ + evaluateAll(): SLAEvaluation[] { + return Array.from(this.checks.keys()) + .map((id) => this.evaluate(id)) + .sort((a, b) => { + const order: Record = { fail: 0, warn: 1, pass: 2 }; + return order[a.overall] - order[b.overall]; + }); + } + + // ─── Reporting ───────────────────────────────────────────────────────────── + + /** + * Generate a structured SLA report for a provider. + */ + generateReport(providerId: ProviderId): SLAProviderReport { + const evaluation = this.evaluate(providerId); + const checks = this.getWindowChecks(providerId); + + const windows: UptimeWindow[] = [ + computeUptimeWindow( + checks.filter((c) => c.timestamp >= new Date(Date.now() - 3_600_000)), + '1h', + 3_600_000, + ), + computeUptimeWindow( + checks.filter((c) => c.timestamp >= new Date(Date.now() - 86_400_000)), + '24h', + 86_400_000, + ), + computeUptimeWindow(checks, `${this.windowMs / 3_600_000}h`, this.windowMs), + ]; + + const recommendations = buildRecommendations(evaluation.breakdown); + + const overall = evaluation.overall.toUpperCase(); + const summary = `Provider ${providerId} SLA evaluation: ${overall}. Uptime ${evaluation.uptime.uptimePct.toFixed(2)}%, avg response ${evaluation.responseTime.avgMs}ms, success rate ${(evaluation.successRate * 100).toFixed(1)}%.`; + + return { + reportId: `sla-${providerId}-${Date.now()}`, + providerId, + generatedAt: new Date(), + periodMs: this.windowMs, + evaluation, + windows, + recommendations, + summary, + }; + } + + /** + * Generate reports for all tracked providers. + */ + generateAllReports(): SLAProviderReport[] { + return Array.from(this.checks.keys()).map((id) => this.generateReport(id)); + } + + // ─── Introspection ───────────────────────────────────────────────────────── + + /** Return IDs of all tracked providers. */ + getProviderIds(): ProviderId[] { + return Array.from(this.checks.keys()); + } + + /** Return the raw checks stored for a provider (within the window). */ + getChecks(providerId: ProviderId): UptimeCheck[] { + return this.getWindowChecks(providerId); + } + + /** Clear all recorded checks for a provider. */ + clearProvider(providerId: ProviderId): void { + this.checks.delete(providerId); + this.prevOverall.delete(providerId); + } + + // ─── Private Helpers ─────────────────────────────────────────────────────── + + private getWindowChecks(providerId: ProviderId): UptimeCheck[] { + const cutoff = new Date(Date.now() - this.windowMs); + return (this.checks.get(providerId) ?? []).filter((c) => c.timestamp >= cutoff); + } + + private pruneOld(providerId: ProviderId): void { + const cutoff = new Date(Date.now() - this.windowMs * 2); + const current = this.checks.get(providerId) ?? []; + this.checks.set( + providerId, + current.filter((c) => c.timestamp >= cutoff), + ); + } + + private handleTransitions( + providerId: ProviderId, + overall: ComplianceVerdict, + evaluation: SLAEvaluation, + ): void { + const prev = this.prevOverall.get(providerId); + if (overall === 'fail' && prev !== 'fail') { + this.onBreach?.(evaluation); + } else if (overall === 'pass' && prev === 'fail') { + this.onRecovery?.(providerId); + } + this.prevOverall.set(providerId, overall); + } +} diff --git a/src/monitoring/sla/providers/stellar/types.ts b/src/monitoring/sla/providers/stellar/types.ts new file mode 100644 index 00000000..a5ef8e1d --- /dev/null +++ b/src/monitoring/sla/providers/stellar/types.ts @@ -0,0 +1,130 @@ +/** + * Stellar Provider SLA Evaluator Types + * + * Defines types for evaluating bridge providers against SLA targets, + * tracking uptime metrics, response times, and generating SLA reports. + * + * @see Issue #469 — Implement Stellar Provider SLA Evaluator + */ + +// ─── Provider Identity ──────────────────────────────────────────────────────── + +/** Unique identifier for a bridge provider. */ +export type ProviderId = string; + +// ─── SLA Targets ───────────────────────────────────────────────────────────── + +/** Target SLA thresholds that a provider must meet. */ +export interface SLATargets { + /** Minimum uptime percentage (0–100). Default: 99.5 */ + minUptimePct: number; + /** Maximum acceptable average response time in ms. Default: 2000 */ + maxAvgResponseMs: number; + /** Maximum acceptable p99 response time in ms. Default: 8000 */ + maxP99ResponseMs: number; + /** Minimum success rate (0–1). Default: 0.97 */ + minSuccessRate: number; +} + +/** Default SLA targets applied when none are specified. */ +export const DEFAULT_SLA_TARGETS: SLATargets = { + minUptimePct: 99.5, + maxAvgResponseMs: 2_000, + maxP99ResponseMs: 8_000, + minSuccessRate: 0.97, +}; + +// ─── Uptime Tracking ────────────────────────────────────────────────────────── + +/** A single uptime check result for a provider. */ +export interface UptimeCheck { + /** When the check was performed. */ + timestamp: Date; + /** Whether the provider responded successfully. */ + available: boolean; + /** Round-trip response time in ms (undefined if unavailable). */ + responseMs?: number; + /** Error message if the check failed. */ + error?: string; +} + +/** Rolling uptime window metrics. */ +export interface UptimeWindow { + /** Window label (e.g. "1h", "24h", "7d"). */ + label: string; + /** Duration of the window in milliseconds. */ + durationMs: number; + /** Number of checks in this window. */ + totalChecks: number; + /** Number of successful checks. */ + successfulChecks: number; + /** Uptime percentage for this window. */ + uptimePct: number; +} + +// ─── Response Time Metrics ──────────────────────────────────────────────────── + +/** Computed response-time statistics for a measurement window. */ +export interface ResponseTimeStats { + avgMs: number; + p50Ms: number; + p95Ms: number; + p99Ms: number; + minMs: number; + maxMs: number; +} + +// ─── SLA Evaluation Result ──────────────────────────────────────────────────── + +/** SLA compliance verdict for a single metric. */ +export type ComplianceVerdict = 'pass' | 'warn' | 'fail'; + +/** Breakdown of compliance for each SLA metric. */ +export interface SLABreakdown { + uptime: { verdict: ComplianceVerdict; actual: number; target: number }; + avgResponse: { verdict: ComplianceVerdict; actual: number; target: number }; + p99Response: { verdict: ComplianceVerdict; actual: number; target: number }; + successRate: { verdict: ComplianceVerdict; actual: number; target: number }; +} + +/** Full SLA evaluation result for a provider. */ +export interface SLAEvaluation { + providerId: ProviderId; + evaluatedAt: Date; + windowMs: number; + targets: SLATargets; + uptime: UptimeWindow; + responseTime: ResponseTimeStats; + successRate: number; + breakdown: SLABreakdown; + /** Overall SLA status derived from breakdown verdicts. */ + overall: ComplianceVerdict; +} + +// ─── SLA Report ─────────────────────────────────────────────────────────────── + +/** A generated SLA report for a provider over a specified period. */ +export interface SLAProviderReport { + reportId: string; + providerId: ProviderId; + generatedAt: Date; + periodMs: number; + evaluation: SLAEvaluation; + windows: UptimeWindow[]; + recommendations: string[]; + summary: string; +} + +// ─── Evaluator Config ───────────────────────────────────────────────────────── + +/** Configuration for the StellarProviderSlaEvaluator. */ +export interface SLAEvaluatorConfig { + /** Evaluation window in ms. Default: 86_400_000 (24 h). */ + windowMs?: number; + /** Custom SLA targets (merged with defaults). */ + targets?: Partial; + /** Callback invoked when a provider breaches an SLA target. */ + onBreach?: (evaluation: SLAEvaluation) => void; + /** Callback invoked when a provider recovers (was failing, now passing). */ + onRecovery?: (providerId: ProviderId) => void; +} diff --git a/src/routing/fallbacks/stellar/index.ts b/src/routing/fallbacks/stellar/index.ts new file mode 100644 index 00000000..61c44b2c --- /dev/null +++ b/src/routing/fallbacks/stellar/index.ts @@ -0,0 +1,18 @@ +/** + * Soroban Route Fallback Planner Module + * + * @see Issue #470 — Implement Soroban Route Fallback Planner + */ + +export { SorobanRouteFallbackPlanner } from './soroban-route-fallback-planner'; + +export type { + FallbackReason, + FallbackRankingWeights, + RankedFallbackRoute, + FallbackPlanResult, + FailoverPolicy, + FallbackPlannerConfig, +} from './types'; + +export { DEFAULT_FALLBACK_WEIGHTS, DEFAULT_FAILOVER_POLICY } from './types'; diff --git a/src/routing/fallbacks/stellar/soroban-route-fallback-planner.ts b/src/routing/fallbacks/stellar/soroban-route-fallback-planner.ts new file mode 100644 index 00000000..b8bbefa6 --- /dev/null +++ b/src/routing/fallbacks/stellar/soroban-route-fallback-planner.ts @@ -0,0 +1,225 @@ +/** + * Soroban Route Fallback Planner + * + * Generates alternative routes when preferred routes fail. Ranks candidates + * by a weighted score of fee, speed, and provider reliability, and supports + * configurable automatic failover policies. + * + * @see Issue #470 — Implement Soroban Route Fallback Planner + */ + +import type { Route } from '../../smart/stellar/soroban-smart-routing-engine'; + +import type { + FallbackPlannerConfig, + FallbackPlanResult, + FallbackRankingWeights, + FallbackReason, + FailoverPolicy, + RankedFallbackRoute, +} from './types'; + +import { + DEFAULT_FALLBACK_WEIGHTS, + DEFAULT_FAILOVER_POLICY, +} from './types'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function normalizeWeights(w: FallbackRankingWeights): FallbackRankingWeights { + const total = w.fee + w.speed + w.reliability; + if (Math.abs(total - 1) < 1e-9 || total === 0) return { ...w }; + return { + fee: w.fee / total, + speed: w.speed / total, + reliability: w.reliability / total, + }; +} + +/** Score a route's fee (lower fee → higher score, capped at 100 units baseline). */ +function scoreFee(route: Route): number { + return Math.max(0, Math.min(1, 1 - route.estimatedFee / 100)); +} + +/** Score a route's speed (lower latency → higher score, 300 s baseline). */ +function scoreSpeed(route: Route): number { + return Math.max(0, Math.min(1, 1 - route.estimatedTimeMs / 300_000)); +} + +function rankRoute( + route: Route, + weights: FallbackRankingWeights, + reliabilityScores: Map, +): RankedFallbackRoute { + const feeScore = scoreFee(route); + const speedScore = scoreSpeed(route); + const reliabilityScore = reliabilityScores.get(route.provider) ?? 0.8; + const score = + feeScore * weights.fee + + speedScore * weights.speed + + reliabilityScore * weights.reliability; + + return { + route, + score: Math.max(0, Math.min(1, score)), + breakdown: { feeScore, speedScore, reliabilityScore }, + }; +} + +function shouldAutoFailover( + policy: FailoverPolicy, + reason: FallbackReason, + alternatives: RankedFallbackRoute[], +): boolean { + if (!policy.autoFailover) return false; + if (alternatives.length === 0) return false; + if (policy.autoFailoverReasons && !policy.autoFailoverReasons.includes(reason)) return false; + return true; +} + +// ─── Planner ───────────────────────────────────────────────────────────────── + +export class SorobanRouteFallbackPlanner { + private readonly routes: Route[] = []; + private readonly reliabilityScores = new Map(); + private readonly weights: FallbackRankingWeights; + private readonly policy: FailoverPolicy; + private readonly onFallback?: FallbackPlannerConfig['onFallback']; + private readonly onError?: FallbackPlannerConfig['onError']; + + constructor(config: FallbackPlannerConfig = {}) { + this.weights = normalizeWeights({ + ...DEFAULT_FALLBACK_WEIGHTS, + ...config.rankingWeights, + }); + this.policy = { ...DEFAULT_FAILOVER_POLICY, ...config.failoverPolicy }; + this.onFallback = config.onFallback; + this.onError = config.onError; + } + + // ─── Route Registration ──────────────────────────────────────────────────── + + /** + * Register candidate routes that can be used as fallbacks. + * Duplicates (same route id) are ignored. + */ + registerRoutes(routes: Route[]): void { + for (const route of routes) { + if (!this.routes.some((r) => r.id === route.id)) { + this.routes.push(route); + } + } + } + + /** + * Remove all registered routes. + */ + clearRoutes(): void { + this.routes.length = 0; + } + + /** + * Get all registered routes. + */ + getRoutes(): Route[] { + return [...this.routes]; + } + + // ─── Reliability ─────────────────────────────────────────────────────────── + + /** + * Update the reliability score for a provider (clamped 0–1). + */ + updateReliability(providerId: string, score: number): void { + this.reliabilityScores.set(providerId, Math.max(0, Math.min(1, score))); + } + + /** + * Get the reliability score for a provider (default 0.8). + */ + getReliability(providerId: string): number { + return this.reliabilityScores.get(providerId) ?? 0.8; + } + + // ─── Planning ────────────────────────────────────────────────────────────── + + /** + * Generate a fallback plan for a failed route. + * + * The planner: + * 1. Excludes the failed route's provider (and any extra excluded providers). + * 2. Filters candidates to the same source/destination chains. + * 3. Excludes routes below the minimum reliability threshold. + * 4. Ranks remaining candidates by weighted score. + * 5. Returns the top N alternatives based on the failover policy. + * + * @param failedRoute The route that could not be executed. + * @param reason Why the route failed. + * @param excludeProviders Additional provider IDs to exclude from consideration. + */ + plan( + failedRoute: Route, + reason: FallbackReason, + excludeProviders: string[] = [], + ): FallbackPlanResult { + try { + const excluded = new Set([failedRoute.provider, ...excludeProviders]); + + const candidates = this.routes.filter( + (r) => + r.id !== failedRoute.id && + !excluded.has(r.provider) && + r.sourceChain === failedRoute.sourceChain && + r.destinationChain === failedRoute.destinationChain, + ); + + // Score and filter by reliability threshold + const ranked = candidates + .map((r) => rankRoute(r, this.weights, this.reliabilityScores)) + .filter((r) => r.breakdown.reliabilityScore >= this.policy.minReliabilityThreshold) + .sort((a, b) => b.score - a.score) + .slice(0, this.policy.maxAlternatives); + + const autoFailover = shouldAutoFailover(this.policy, reason, ranked); + + const result: FallbackPlanResult = { + failedRoute, + reason, + alternatives: ranked, + best: ranked[0] ?? null, + excludedProviders: Array.from(excluded), + shouldAutoFailover: autoFailover, + }; + + this.onFallback?.(result); + return result; + } catch (err) { + this.onError?.(err); + return { + failedRoute, + reason, + alternatives: [], + best: null, + excludedProviders: [failedRoute.provider, ...excludeProviders], + shouldAutoFailover: false, + }; + } + } + + /** + * Convenience method: plan a fallback and return only the best route, + * or null if no alternatives are available. + */ + planBest(failedRoute: Route, reason: FallbackReason, excludeProviders?: string[]): Route | null { + return this.plan(failedRoute, reason, excludeProviders).best?.route ?? null; + } + + /** + * Mark a provider as temporarily unavailable. + * Sets its reliability score to 0 so it is excluded from all fallback plans + * until its score is updated via `updateReliability`. + */ + markProviderUnavailable(providerId: string): void { + this.reliabilityScores.set(providerId, 0); + } +} diff --git a/src/routing/fallbacks/stellar/types.ts b/src/routing/fallbacks/stellar/types.ts new file mode 100644 index 00000000..62987161 --- /dev/null +++ b/src/routing/fallbacks/stellar/types.ts @@ -0,0 +1,108 @@ +/** + * Soroban Route Fallback Planner Types + * + * Defines types for generating alternative routes when preferred routes fail, + * ranking alternatives, and supporting automatic failover. + * + * @see Issue #470 — Implement Soroban Route Fallback Planner + */ + +import type { Route } from '../../smart/stellar/soroban-smart-routing-engine'; + +// ─── Fallback Reason ────────────────────────────────────────────────────────── + +/** Reason a preferred route was rejected and a fallback was needed. */ +export type FallbackReason = + | 'provider_unavailable' + | 'execution_timeout' + | 'insufficient_liquidity' + | 'slippage_exceeded' + | 'fee_spike' + | 'manual_override'; + +// ─── Ranking Criteria ───────────────────────────────────────────────────────── + +/** Scoring dimensions used to rank fallback routes. */ +export interface FallbackRankingWeights { + /** Weight for fee scoring (lower fee = better). Range 0–1. */ + fee: number; + /** Weight for speed scoring (lower latency = better). Range 0–1. */ + speed: number; + /** Weight for provider reliability scoring. Range 0–1. */ + reliability: number; +} + +/** Default ranking weights favour reliability in fallback scenarios. */ +export const DEFAULT_FALLBACK_WEIGHTS: FallbackRankingWeights = { + fee: 0.2, + speed: 0.3, + reliability: 0.5, +}; + +// ─── Ranked Fallback ────────────────────────────────────────────────────────── + +/** A route candidate with its computed fallback score. */ +export interface RankedFallbackRoute { + route: Route; + /** Composite score (0–1, higher is better). */ + score: number; + /** Per-dimension score breakdown. */ + breakdown: { + feeScore: number; + speedScore: number; + reliabilityScore: number; + }; +} + +// ─── Planner Result ─────────────────────────────────────────────────────────── + +/** Result returned by the fallback planner. */ +export interface FallbackPlanResult { + /** The route that triggered the fallback. */ + failedRoute: Route; + /** Reason the primary route was rejected. */ + reason: FallbackReason; + /** Ranked list of alternative routes (best first). */ + alternatives: RankedFallbackRoute[]; + /** The best alternative, or null if none are available. */ + best: RankedFallbackRoute | null; + /** Providers excluded from consideration during this plan. */ + excludedProviders: string[]; + /** Whether automatic failover should be attempted. */ + shouldAutoFailover: boolean; +} + +// ─── Failover Policy ────────────────────────────────────────────────────────── + +/** Policy controlling when automatic failover is triggered. */ +export interface FailoverPolicy { + /** Automatically failover when fallbacks are available. Default: true */ + autoFailover: boolean; + /** Minimum reliability score an alternative must have to be eligible. Default: 0.5 */ + minReliabilityThreshold: number; + /** Maximum number of alternatives to return. Default: 3 */ + maxAlternatives: number; + /** Reasons that are eligible for automatic failover. Defaults to all reasons. */ + autoFailoverReasons?: FallbackReason[]; +} + +/** Default failover policy. */ +export const DEFAULT_FAILOVER_POLICY: FailoverPolicy = { + autoFailover: true, + minReliabilityThreshold: 0.5, + maxAlternatives: 3, +}; + +// ─── Planner Config ─────────────────────────────────────────────────────────── + +/** Configuration for SorobanRouteFallbackPlanner. */ +export interface FallbackPlannerConfig { + /** Ranking weights for alternative routes. */ + rankingWeights?: Partial; + /** Failover policy. */ + failoverPolicy?: Partial; + /** Callback invoked when a fallback plan is executed. */ + onFallback?: (result: FallbackPlanResult) => void; + /** Error handler for unexpected errors. */ + onError?: (err: unknown) => void; +}