v0.2.9-pre.003-fix.001

This commit is contained in:
2026-08-24 11:16:04 +02:00
parent a038194679
commit feb9befb35
8 changed files with 1190 additions and 25 deletions

View File

@@ -0,0 +1,398 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_unary.rs
// version: 1
const MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES: usize = 256;
const MAX_YELLOWSTONE_VERSION_LENGTH_BYTES: usize = 512;
const PATH_GET_BLOCK_HEIGHT: &str = "/geyser.Geyser/GetBlockHeight";
const PATH_GET_LATEST_BLOCKHASH: &str = "/geyser.Geyser/GetLatestBlockhash";
const PATH_GET_SLOT: &str = "/geyser.Geyser/GetSlot";
const PATH_GET_VERSION: &str = "/geyser.Geyser/GetVersion";
const PATH_IS_BLOCKHASH_VALID: &str = "/geyser.Geyser/IsBlockhashValid";
const PATH_PING: &str = "/geyser.Geyser/Ping";
const PATH_SUBSCRIBE_REPLAY_INFO: &str = "/geyser.Geyser/SubscribeReplayInfo";
/// Standard Solana Yellowstone unary client layered on one KSP-owned physical gRPC channel.
///
/// This N2 facade deliberately exposes no raw Tonic client and no upstream protobuf types. Streaming `Subscribe` is not part of `0.2.9-pre.003`.
#[derive(Clone)]
pub struct SolanaYellowstoneGrpcUnaryClient {
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
unary_timeout: std::time::Duration,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
}
impl SolanaYellowstoneGrpcUnaryClient {
/// Creates the internal unary facade state from a KSP-owned physical channel and validated endpoint settings.
pub(crate) fn new(
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
settings: crate::YellowstoneGrpcSessionSettings,
) -> Self {
return Self {
channel,
metadata,
unary_timeout: settings.unary_timeout(),
max_inbound_message_size_bytes: settings.max_inbound_message_size_bytes(),
max_outbound_message_size_bytes: settings.max_outbound_message_size_bytes(),
};
}
/// Returns the first slot retained by the endpoint for replay when advertised.
pub async fn subscribe_replay_info(&self) -> ksp_core_lib::Result<crate::YellowstoneReplayInfo> {
let response: yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse =
match self.unary("SubscribeReplayInfo", PATH_SUBSCRIBE_REPLAY_INFO, yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest {}).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneReplayInfo::new(response.first_available));
}
/// Executes the standard Yellowstone unary `Ping` RPC and verifies the echoed count.
pub async fn ping(&self, count: i32) -> ksp_core_lib::Result<crate::YellowstonePong> {
let response: yellowstone_grpc_proto::geyser::PongResponse =
match self.unary("Ping", PATH_PING, yellowstone_grpc_proto::geyser::PingRequest { count }).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if response.count != count {
return invalid_unary_response("Ping", "Yellowstone Ping response count does not match the request");
}
return std::result::Result::Ok(crate::YellowstonePong::new(response.count));
}
/// Returns the latest blockhash advertised by the Yellowstone endpoint at an optional commitment.
pub async fn get_latest_blockhash(
&self,
commitment: std::option::Option<crate::SolanaCommitment>,
) -> ksp_core_lib::Result<crate::YellowstoneLatestBlockhash> {
let response: yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse = match self
.unary(
"GetLatestBlockhash",
PATH_GET_LATEST_BLOCKHASH,
yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest { commitment: commitment_to_wire(commitment) },
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if response.blockhash.is_empty() || response.blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES {
return invalid_unary_response("GetLatestBlockhash", "Yellowstone latest blockhash response violates the KSP text bound");
}
return std::result::Result::Ok(crate::YellowstoneLatestBlockhash::new(response.slot, response.blockhash, response.last_valid_block_height));
}
/// Returns the current block height at an optional commitment.
pub async fn get_block_height(&self, commitment: std::option::Option<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneBlockHeight> {
let response: yellowstone_grpc_proto::geyser::GetBlockHeightResponse = match self
.unary(
"GetBlockHeight",
PATH_GET_BLOCK_HEIGHT,
yellowstone_grpc_proto::geyser::GetBlockHeightRequest { commitment: commitment_to_wire(commitment) },
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneBlockHeight::new(response.block_height));
}
/// Returns the current slot at an optional commitment.
pub async fn get_slot(&self, commitment: std::option::Option<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneSlot> {
let response: yellowstone_grpc_proto::geyser::GetSlotResponse =
match self.unary("GetSlot", PATH_GET_SLOT, yellowstone_grpc_proto::geyser::GetSlotRequest { commitment: commitment_to_wire(commitment) }).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneSlot::new(response.slot));
}
/// Checks one opaque Solana blockhash at an optional commitment.
pub async fn is_blockhash_valid(
&self,
blockhash: impl std::convert::Into<std::string::String>,
commitment: std::option::Option<crate::SolanaCommitment>,
) -> ksp_core_lib::Result<crate::YellowstoneBlockhashValidity> {
let blockhash = blockhash.into();
if blockhash.is_empty() || blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES || blockhash.trim() != blockhash {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone blockhash must be non-empty, bounded and untrimmed")
.with_context("grpc_operation", "IsBlockhashValid")
.with_context("field", "blockhash"),
);
}
let response: yellowstone_grpc_proto::geyser::IsBlockhashValidResponse = match self
.unary(
"IsBlockhashValid",
PATH_IS_BLOCKHASH_VALID,
yellowstone_grpc_proto::geyser::IsBlockhashValidRequest { blockhash, commitment: commitment_to_wire(commitment) },
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneBlockhashValidity::new(response.slot, response.valid));
}
/// Returns the endpoint's Yellowstone/validator version string after applying a deterministic KSP text bound.
pub async fn get_version(&self) -> ksp_core_lib::Result<crate::YellowstoneVersionInfo> {
let response: yellowstone_grpc_proto::geyser::GetVersionResponse =
match self.unary("GetVersion", PATH_GET_VERSION, yellowstone_grpc_proto::geyser::GetVersionRequest {}).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if response.version.is_empty() || response.version.len() > MAX_YELLOWSTONE_VERSION_LENGTH_BYTES {
return invalid_unary_response("GetVersion", "Yellowstone version response violates the KSP text bound");
}
return std::result::Result::Ok(crate::YellowstoneVersionInfo::new(response.version));
}
async fn unary<Request, Response>(&self, operation: &'static str, path: &'static str, message: Request) -> ksp_core_lib::Result<Response>
where
Request: tonic_prost::prost::Message + Send + Sync + 'static,
Response: tonic_prost::prost::Message + std::default::Default + Send + Sync + 'static,
{
let path = match path.parse::<http::uri::PathAndQuery>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "internal Yellowstone gRPC method path is invalid")
.with_context("grpc_operation", operation),
);
},
};
let mut grpc = tonic::client::Grpc::new(self.channel.clone())
.max_decoding_message_size(self.max_inbound_message_size_bytes)
.max_encoding_message_size(self.max_outbound_message_size_bytes);
let future = async {
if grpc.ready().await.is_err() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel is not ready for unary dispatch")
.with_context("grpc_operation", operation),
);
}
let mut request = tonic::Request::new(message);
for entry in &self.metadata {
if let std::result::Result::Err(error) = entry.append_to(request.metadata_mut()) {
return std::result::Result::Err(error);
}
}
let response = match grpc.unary(request, path, tonic_prost::ProstCodec::<Request, Response>::default()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(status) => return std::result::Result::Err(grpc_status_error(operation, status)),
};
return std::result::Result::Ok(response.into_inner());
};
return match tokio::time::timeout(self.unary_timeout, future).await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, "Yellowstone gRPC unary operation exceeded the KSP deadline")
.with_context("grpc_operation", operation),
),
};
}
}
impl std::fmt::Debug for SolanaYellowstoneGrpcUnaryClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("SolanaYellowstoneGrpcUnaryClient")
.field("metadata_count", &self.metadata.len())
.field("unary_timeout", &self.unary_timeout)
.field("max_inbound_message_size_bytes", &self.max_inbound_message_size_bytes)
.field("max_outbound_message_size_bytes", &self.max_outbound_message_size_bytes)
.field("channel", &"<private>")
.finish();
}
}
/// Replay availability advertised by `SubscribeReplayInfo`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneReplayInfo {
first_available: std::option::Option<u64>,
}
impl YellowstoneReplayInfo {
/// Creates a replay-info projection.
#[must_use]
pub const fn new(first_available: std::option::Option<u64>) -> Self {
return Self { first_available };
}
/// Returns the first replayable slot when the endpoint advertises one.
#[must_use]
pub const fn first_available(&self) -> std::option::Option<u64> {
return self.first_available;
}
}
/// Echo returned by the Yellowstone unary `Ping` RPC.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstonePong {
count: i32,
}
impl YellowstonePong {
/// Creates a ping response projection.
#[must_use]
pub const fn new(count: i32) -> Self {
return Self { count };
}
/// Returns the echoed ping count.
#[must_use]
pub const fn count(&self) -> i32 {
return self.count;
}
}
/// Latest blockhash response returned by Yellowstone.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneLatestBlockhash {
slot: u64,
blockhash: std::string::String,
last_valid_block_height: u64,
}
impl YellowstoneLatestBlockhash {
/// Creates a latest-blockhash projection.
#[must_use]
pub fn new(slot: u64, blockhash: std::string::String, last_valid_block_height: u64) -> Self {
return Self { slot, blockhash, last_valid_block_height };
}
/// Returns the response slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the opaque Solana blockhash text.
#[must_use]
pub fn blockhash(&self) -> &str {
return self.blockhash.as_str();
}
/// Returns the last valid block height associated with the blockhash.
#[must_use]
pub const fn last_valid_block_height(&self) -> u64 {
return self.last_valid_block_height;
}
}
/// Current block height returned by Yellowstone.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneBlockHeight {
block_height: u64,
}
impl YellowstoneBlockHeight {
/// Creates a block-height projection.
#[must_use]
pub const fn new(block_height: u64) -> Self {
return Self { block_height };
}
/// Returns the block height.
#[must_use]
pub const fn block_height(&self) -> u64 {
return self.block_height;
}
}
/// Current slot returned by Yellowstone.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneSlot {
slot: u64,
}
impl YellowstoneSlot {
/// Creates a slot projection.
#[must_use]
pub const fn new(slot: u64) -> Self {
return Self { slot };
}
/// Returns the slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
}
/// Result of checking one blockhash with Yellowstone.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneBlockhashValidity {
slot: u64,
valid: bool,
}
impl YellowstoneBlockhashValidity {
/// Creates a blockhash-validity projection.
#[must_use]
pub const fn new(slot: u64, valid: bool) -> Self {
return Self { slot, valid };
}
/// Returns the response slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns whether the blockhash is valid at the requested commitment.
#[must_use]
pub const fn valid(&self) -> bool {
return self.valid;
}
}
/// Bounded Yellowstone endpoint version information.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneVersionInfo {
version: std::string::String,
}
impl YellowstoneVersionInfo {
/// Creates a version projection.
#[must_use]
pub fn new(version: std::string::String) -> Self {
return Self { version };
}
/// Returns the bounded endpoint version string.
#[must_use]
pub fn version(&self) -> &str {
return self.version.as_str();
}
}
fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>) -> std::option::Option<i32> {
return commitment.map(|value| {
return match value {
crate::SolanaCommitment::Processed => yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32,
crate::SolanaCommitment::Confirmed => yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed as i32,
crate::SolanaCommitment::Finalized => yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32,
};
});
}
fn grpc_status_error(operation: &'static str, status: tonic::Status) -> ksp_core_lib::Error {
let code = status.code().to_string();
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, grpc_operation = operation, grpc_code = code.as_str(), "Yellowstone gRPC unary endpoint returned a status");
return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_STATUS, "Yellowstone gRPC endpoint returned a gRPC status")
.with_context("grpc_operation", operation)
.with_context("grpc_code", code);
}
fn invalid_unary_response<T>(operation: &'static str, message: &str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("grpc_operation", operation));
}
#[cfg(test)]
#[path = "../unit_tests/grpc_unary.rs"]
mod tests;

View File

@@ -0,0 +1,288 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
// version: 2
#[derive(Clone, Default)]
struct FixtureGeyser;
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
#[tonic::async_trait]
impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
type SubscribeStream = std::pin::Pin<
std::boxed::Box<dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>> + Send + 'static>,
>;
type SubscribeDeshredStream = std::pin::Pin<
std::boxed::Box<
dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>> + Send + 'static,
>,
>;
async fn subscribe(
&self,
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("streaming is outside the pre.003 fixture"));
}
async fn subscribe_deshred(
&self,
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside KSP 0.2.9"));
}
async fn subscribe_replay_info(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse {
first_available: std::option::Option::Some(100),
}));
}
async fn ping(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
let count = request.into_inner().count;
if count == -999 {
return std::result::Result::Err(tonic::Status::permission_denied("GRPC-REMOTE-SECRET-CANARY"));
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::PongResponse { count }));
}
async fn get_latest_blockhash(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse {
slot: 101,
blockhash: "fixture-blockhash".to_owned(),
last_valid_block_height: 999,
}));
}
async fn get_block_height(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetBlockHeightResponse { block_height: 202 }));
}
async fn get_slot(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetSlotRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if request.get_ref().commitment == std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32) {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
} else if let std::result::Result::Err(error) =
require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetSlotResponse { slot: 303 }));
}
async fn is_blockhash_valid(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
return std::result::Result::Err(error);
}
if request.get_ref().blockhash != "fixture-blockhash" {
return std::result::Result::Err(tonic::Status::invalid_argument("unexpected fixture blockhash"));
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::IsBlockhashValidResponse { slot: 404, valid: true }));
}
async fn get_version(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetVersionRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetVersionResponse {
version: "fixture-yellowstone-12.6".to_owned(),
}));
}
}
struct FixtureServer {
endpoint_url: std::string::String,
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
task: tokio::task::JoinHandle<()>,
}
impl FixtureServer {
async fn start() -> Self {
let bind_address: std::net::SocketAddr = "127.0.0.1:0".parse().expect("fixture bind address must parse");
let incoming = tonic::transport::server::TcpIncoming::bind(bind_address).expect("fixture gRPC listener must bind");
let local_address = incoming.local_addr().expect("fixture gRPC listener must expose local address");
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
let task = tokio::spawn(async move {
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser);
let result = tonic::transport::Server::builder()
.serve_with_incoming_shutdown(service, incoming, async move {
let _ = shutdown_receiver.await;
})
.await;
assert!(result.is_ok());
});
return Self { endpoint_url: format!("http://{local_address}"), shutdown: std::option::Option::Some(shutdown), task };
}
async fn stop(mut self) {
if let std::option::Option::Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
let result = self.task.await;
assert!(result.is_ok());
}
}
fn verify_fixture_metadata(metadata: &tonic::metadata::MetadataMap) -> std::result::Result<(), tonic::Status> {
let public = metadata.get("x-ksp-public").and_then(|value| return value.to_str().ok());
let secret = metadata.get("x-ksp-token").and_then(|value| return value.to_str().ok());
if public != std::option::Option::Some("fixture-public") || secret != std::option::Option::Some("GRPC-SECRET-CANARY") {
return std::result::Result::Err(tonic::Status::unauthenticated("fixture metadata mismatch"));
}
return std::result::Result::Ok(());
}
fn require_commitment(actual: std::option::Option<i32>, expected: yellowstone_grpc_proto::geyser::CommitmentLevel) -> std::result::Result<(), tonic::Status> {
if actual != std::option::Option::Some(expected as i32) {
return std::result::Result::Err(tonic::Status::invalid_argument("fixture commitment mismatch"));
}
return std::result::Result::Ok(());
}
fn fixture_settings(url: &str, unary_timeout: std::time::Duration) -> crate::YellowstoneGrpcEndpointSettings {
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let session = crate::YellowstoneGrpcSessionSettings::new(
defaults.connect_timeout(),
unary_timeout,
defaults.close_timeout(),
defaults.reconnect().clone(),
defaults.request_channel_capacity(),
defaults.update_channel_capacity(),
defaults.max_inbound_message_size_bytes(),
defaults.max_outbound_message_size_bytes(),
);
let metadata = std::vec![
crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture-public").expect("fixture public metadata must be valid"),
crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("fixture secret metadata must be valid"),
];
return crate::YellowstoneGrpcEndpointSettings::new(
"fixture",
true,
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
crate::YellowstoneGrpcClusterName::new("devnet"),
crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture URL must parse"),
session,
)
.with_metadata(metadata)
.expect("fixture metadata settings must validate");
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_unary_fixture_covers_all_seven_standard_methods_and_metadata() {
let server = FixtureServer::start().await;
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1));
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let client = channel.standard_unary_client();
let replay = client.subscribe_replay_info().await.expect("ReplayInfo fixture must succeed");
assert_eq!(replay.first_available(), std::option::Option::Some(100));
let pong = client.ping(7).await.expect("Ping fixture must succeed");
assert_eq!(pong.count(), 7);
let latest = client
.get_latest_blockhash(std::option::Option::Some(crate::SolanaCommitment::Confirmed))
.await
.expect("latest blockhash fixture must succeed");
assert_eq!(latest.slot(), 101);
assert_eq!(latest.blockhash(), "fixture-blockhash");
assert_eq!(latest.last_valid_block_height(), 999);
let height = client.get_block_height(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("block height fixture must succeed");
assert_eq!(height.block_height(), 202);
let slot = client.get_slot(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("slot fixture must succeed");
assert_eq!(slot.slot(), 303);
let validity = client
.is_blockhash_valid("fixture-blockhash", std::option::Option::Some(crate::SolanaCommitment::Confirmed))
.await
.expect("blockhash validity fixture must succeed");
assert_eq!(validity.slot(), 404);
assert!(validity.valid());
let version = client.get_version().await.expect("version fixture must succeed");
assert_eq!(version.version(), "fixture-yellowstone-12.6");
let rendered = format!("{client:?} {channel:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_remote_status_does_not_copy_message_details_or_metadata() {
let server = FixtureServer::start().await;
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1));
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let error = channel.standard_unary_client().ping(-999).await.expect_err("fixture must return remote status");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
let rendered = format!("{error:?}");
assert!(!rendered.contains("GRPC-REMOTE-SECRET-CANARY"));
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_unary_timeout_is_bounded_and_safe() {
let server = FixtureServer::start().await;
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_millis(5));
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let error = channel
.standard_unary_client()
.get_slot(std::option::Option::Some(crate::SolanaCommitment::Processed))
.await
.expect_err("delayed fixture must exceed unary timeout");
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
let rendered = format!("{error:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_invalid_blockhash_is_rejected_before_io() {
let settings = fixture_settings("http://127.0.0.1:1", std::time::Duration::from_secs(1));
let channel = crate::YellowstoneGrpcChannel::prepare(&settings).expect("lazy fixture channel must prepare");
let error = channel
.standard_unary_client()
.is_blockhash_valid(" ", std::option::Option::None)
.await
.expect_err("invalid blockhash must be rejected locally");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
}