//! Unified gRPC client for the Foxhunt API Gateway. //! //! All service RPCs route through a single [`FoxhuntClient`] which holds //! one lazily-connected [`Channel`] to the API Gateway. Individual typed //! clients (trading, ML, risk, ...) are constructed on demand from the //! shared channel, each wrapped with [`AuthInterceptor`] so every outgoing //! RPC carries the JWT Bearer token when available. use anyhow::Result; use tonic::service::interceptor::InterceptedService; use tonic::transport::Channel; use crate::auth::interceptor::AuthInterceptor; use crate::auth::token_manager::{AuthTokenManager, FileTokenStorage}; /// Channel wrapped with the JWT auth interceptor. type AuthChannel = InterceptedService>; /// Unified gRPC client -- all service accessors share a single channel /// and automatically inject JWT auth headers via [`AuthInterceptor`]. pub struct FoxhuntClient { channel: Channel, interceptor: AuthInterceptor, } impl FoxhuntClient { /// Create a client that will lazily connect to the API Gateway. /// /// Sets up the auth interceptor backed by [`FileTokenStorage`] so all /// outgoing RPCs include the Bearer token when one is cached on disk. /// /// The TCP handshake is deferred until the first RPC call (`connect_lazy`). /// /// # Errors /// /// Returns an error if the URL cannot be parsed or token storage init fails. pub async fn connect(api_url: &str) -> Result { let mut endpoint = Channel::from_shared(api_url.to_owned()).map_err(|e| anyhow::anyhow!("{e}"))?; // Auto-configure TLS for https endpoints. if api_url.starts_with("https://") { endpoint = endpoint .tls_config(tonic::transport::ClientTlsConfig::new().with_enabled_roots()) .map_err(|e| anyhow::anyhow!("TLS config error: {e}"))?; } let channel = endpoint.connect_lazy(); // Auth interceptor: reads cached JWT from ~/.config/foxhunt-fxt/tokens/ // and injects "Authorization: Bearer " on every RPC. // If no token is cached, requests proceed without auth (allows login). let storage = FileTokenStorage::new()?; let auth_manager = AuthTokenManager::new(storage); let interceptor = AuthInterceptor::new(auth_manager); Ok(Self { channel, interceptor, }) } /// Get the underlying bare channel (no auth interceptor). /// /// Used by [`LoginClient`] which needs unauthenticated access for the /// initial login RPC before any token exists. pub fn channel(&self) -> Channel { self.channel.clone() } // ── Typed service accessors ────────────────────────────────────── /// Trading service client (order management, positions, executions). pub fn trading( &self, ) -> crate::proto::trading::trading_service_client::TradingServiceClient { crate::proto::trading::trading_service_client::TradingServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// ML inference service client (predictions, ensemble voting). pub fn ml(&self) -> crate::proto::ml::ml_service_client::MlServiceClient { crate::proto::ml::ml_service_client::MlServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// ML training service client (start/stop training, checkpoints). pub fn ml_training( &self, ) -> crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient { crate::proto::ml_training::ml_training_service_client::MlTrainingServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// Broker gateway service client (account info, order routing). pub fn broker_gateway( &self, ) -> crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient { crate::proto::broker_gateway::broker_gateway_service_client::BrokerGatewayServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// Trading agent service client (autonomous agent control). pub fn trading_agent( &self, ) -> crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient { crate::proto::trading_agent::trading_agent_service_client::TradingAgentServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// Monitoring service client (metrics, alerts, system health). pub fn monitoring( &self, ) -> crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient { crate::proto::monitoring::monitoring_service_client::MonitoringServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// Configuration service client (trading service config -- package `config`). pub fn config( &self, ) -> crate::proto::config::config_service_client::ConfigServiceClient { crate::proto::config::config_service_client::ConfigServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// Data acquisition service client (market data feeds, downloads). pub fn data_acquisition( &self, ) -> crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient { crate::proto::data_acquisition::data_acquisition_service_client::DataAcquisitionServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// Risk service client (VaR, position limits, circuit breakers). pub fn risk( &self, ) -> crate::proto::risk::risk_service_client::RiskServiceClient { crate::proto::risk::risk_service_client::RiskServiceClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } /// gRPC health check client (standard `grpc.health.v1.Health`). /// /// Health checks don't require auth, but the interceptor handles the /// no-token case gracefully (proceeds without Bearer header). pub fn health( &self, ) -> crate::proto::health::health_client::HealthClient { crate::proto::health::health_client::HealthClient::with_interceptor( self.channel.clone(), self.interceptor.clone(), ) } }