// file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs // version: 4 /// Prepared or connected Yellowstone gRPC channel owned by KSP Transport. /// /// The underlying Tonic channel, endpoint URL and request metadata remain private. Callers use the KSP-owned typed Yellowstone surfaces layered on this /// physical channel instead of receiving a raw Tonic escape hatch. #[derive(Clone)] pub struct YellowstoneGrpcChannel { endpoint_name: std::string::String, provider: crate::YellowstoneGrpcProviderName, cluster: crate::YellowstoneGrpcClusterName, channel: tonic::transport::Channel, metadata: std::vec::Vec, session: crate::YellowstoneGrpcSessionSettings, } impl YellowstoneGrpcChannel { /// Prepares one lazy HTTP/2 channel without establishing a network connection. /// /// HTTPS endpoints receive the KSP TLS configuration immediately, so invalid local TLS setup is rejected before a typed client is created. Tonic lazy /// channels require an active Tokio runtime even though no socket is opened yet. pub fn prepare(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result { if let std::result::Result::Err(error) = settings.validate() { return std::result::Result::Err(error); } if !settings.enabled() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot prepare a channel") .with_context("endpoint_name", settings.name()), ); } if tokio::runtime::Handle::try_current().is_err() { return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel preparation requires an active Tokio runtime")); } let endpoint = match build_tonic_endpoint(settings) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let channel = endpoint.connect_lazy(); ksp_logging_lib::debug!( target: crate::TRACING_TARGET, endpoint_name = settings.name(), provider = settings.provider().as_str(), cluster = settings.cluster().as_str(), tls = settings.url().uses_tls(), metadata_count = settings.metadata().len(), "prepared lazy Yellowstone gRPC channel" ); return std::result::Result::Ok(Self::from_parts(settings, channel)); } /// Establishes one Yellowstone gRPC HTTP/2 channel with bounded connect timeout and configured TLS roots. pub async fn connect(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result { if let std::result::Result::Err(error) = settings.validate() { return std::result::Result::Err(error); } if !settings.enabled() { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot connect") .with_context("endpoint_name", settings.name()), ); } if tokio::runtime::Handle::try_current().is_err() { return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC connection requires an active Tokio runtime")); } let endpoint = match build_tonic_endpoint(settings) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let channel = match endpoint.connect().await { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, endpoint_name = settings.name(), provider = settings.provider().as_str(), cluster = settings.cluster().as_str(), tls = settings.url().uses_tls(), "Yellowstone gRPC channel connection failed" ); return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel connection failed")); }, }; ksp_logging_lib::debug!( target: crate::TRACING_TARGET, endpoint_name = settings.name(), provider = settings.provider().as_str(), cluster = settings.cluster().as_str(), tls = settings.url().uses_tls(), "connected Yellowstone gRPC channel" ); return std::result::Result::Ok(Self::from_parts(settings, channel)); } /// Returns the safe logical endpoint name. #[must_use] pub fn endpoint_name(&self) -> &str { return self.endpoint_name.as_str(); } /// Returns the open provider descriptor. #[must_use] pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName { return &self.provider; } /// Returns the open cluster descriptor. #[must_use] pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName { return &self.cluster; } /// Creates the standard Solana Yellowstone unary facade over this physical channel. #[must_use] pub fn standard_unary_client(&self) -> crate::SolanaYellowstoneGrpcUnaryClient { return crate::SolanaYellowstoneGrpcUnaryClient::new(self.channel.clone(), self.metadata.clone(), self.session.clone()); } /// Opens one standard Solana Yellowstone bidirectional `Subscribe` session over this physical channel. pub async fn open_standard_subscribe( &self, initial_request: crate::YellowstoneSubscribeRequest, ) -> ksp_core_lib::Result { return crate::open_yellowstone_subscribe_session( self.channel.clone(), self.metadata.clone(), self.session.clone(), self.endpoint_name.clone(), self.provider.clone(), self.cluster.clone(), initial_request, ) .await; } fn from_parts(settings: &crate::YellowstoneGrpcEndpointSettings, channel: tonic::transport::Channel) -> Self { return Self { endpoint_name: settings.name().to_owned(), provider: settings.provider().clone(), cluster: settings.cluster().clone(), channel, metadata: settings.metadata().to_vec(), session: settings.session().clone(), }; } } impl std::fmt::Debug for YellowstoneGrpcChannel { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("YellowstoneGrpcChannel") .field("endpoint_name", &self.endpoint_name) .field("provider", &self.provider) .field("cluster", &self.cluster) .field("metadata_count", &self.metadata.len()) .field("channel", &"") .finish(); } } fn build_tonic_endpoint(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result { let endpoint = match tonic::transport::Endpoint::from_shared(settings.url().as_str().to_owned()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC endpoint URI cannot be prepared by the HTTP/2 transport")); }, }; let endpoint = endpoint.connect_timeout(settings.session().connect_timeout()).buffer_size(settings.session().request_channel_capacity()); if settings.url().uses_tls() { let tls = tonic::transport::ClientTlsConfig::new().with_webpki_roots().timeout(settings.session().connect_timeout()); return match endpoint.tls_config(tls) { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(_) => { std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC TLS configuration failed before connection")) }, }; } return std::result::Result::Ok(endpoint); } fn grpc_channel_error(settings: &crate::YellowstoneGrpcEndpointSettings, message: &str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, message) .with_context("endpoint_name", settings.name()) .with_context("provider", settings.provider().as_str()) .with_context("cluster", settings.cluster().as_str()); } #[cfg(test)] #[path = "../unit_tests/grpc_channel.rs"] mod tests;