- Rename TliConfig→FxtConfig, TliError→FxtError, TliResult→FxtResult - Migrate token storage path foxhunt-tli→foxhunt-fxt with auto-migration - Fix FileTokenStorage::Default panic (fallback to /tmp on missing HOME) - Update all doc comments, login prompt, config.toml.example, env vars - Update keyring service names foxhunt-tli-access→foxhunt-fxt-access - Add TOKEN_REFRESH_BUFFER_SECS named constant - Add clarifying comments for JWT dummy key and IBKR default client ID Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
180 lines
6.8 KiB
Rust
180 lines
6.8 KiB
Rust
//! 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<Channel, AuthInterceptor<FileTokenStorage>>;
|
|
|
|
/// 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<FileTokenStorage>,
|
|
}
|
|
|
|
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<Self> {
|
|
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 <token>" 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<AuthChannel> {
|
|
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<AuthChannel> {
|
|
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<AuthChannel>
|
|
{
|
|
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<AuthChannel>
|
|
{
|
|
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<AuthChannel>
|
|
{
|
|
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<AuthChannel>
|
|
{
|
|
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<AuthChannel> {
|
|
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<AuthChannel>
|
|
{
|
|
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<AuthChannel> {
|
|
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<AuthChannel> {
|
|
crate::proto::health::health_client::HealthClient::with_interceptor(
|
|
self.channel.clone(),
|
|
self.interceptor.clone(),
|
|
)
|
|
}
|
|
|
|
}
|