// file: crates/ksp-onchain-transport-lib/src/rpc_canary.rs // version: 2 /// Optional typed configuration for `getBalance` retained for the `0.2.1` public canary contract. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct GetBalanceConfig { commitment: std::option::Option, min_context_slot: std::option::Option, } impl GetBalanceConfig { /// Creates an explicit `getBalance` configuration. #[must_use] pub const fn new(commitment: std::option::Option, min_context_slot: std::option::Option) -> Self { return Self { commitment, min_context_slot }; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional minimum context slot. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { return self.min_context_slot; } fn is_empty(&self) -> bool { return self.commitment.is_none() && self.min_context_slot.is_none(); } fn to_json_value(&self) -> serde_json::Value { return crate::SolanaContextConfig::new(self.commitment, self.min_context_slot).to_json_value(); } } /// Typed healthy result returned by the `getHealth` canary. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SolanaNodeHealth { /// The RPC node returned the stable `"ok"` health result. Healthy, } /// Typed genesis hash returned by the `getGenesisHash` canary. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct SolanaGenesisHash { value: std::string::String, } impl SolanaGenesisHash { /// Returns the base58-encoded genesis hash text. #[must_use] pub fn as_str(&self) -> &str { return self.value.as_str(); } } /// Typed software-version response returned by the `getVersion` canary. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SolanaNodeVersion { solana_core: std::string::String, feature_set: std::option::Option, } impl SolanaNodeVersion { /// Returns the node software version string from the `solana-core` field. #[must_use] pub fn solana_core(&self) -> &str { return self.solana_core.as_str(); } /// Returns the optional runtime feature-set identifier. #[must_use] pub const fn feature_set(&self) -> std::option::Option { return self.feature_set; } } /// Typed lamport balance returned by the `getBalance` canary. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GetBalanceResult { context: crate::SolanaRpcContext, value: u64, } impl GetBalanceResult { /// Returns the Solana response context. #[must_use] pub const fn context(&self) -> &crate::SolanaRpcContext { return &self.context; } /// Returns the account balance in lamports. #[must_use] pub const fn value(&self) -> u64 { return self.value; } } impl crate::HttpTransportPool { /// Executes the typed `getHealth` foundation canary. pub async fn get_health(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { let method_result = canary_descriptor("getHealth"); let method = match method_result { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = self.execute_standard_rpc(role, method, std::vec::Vec::new()).await; let value = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if value.as_str() == std::option::Option::Some("ok") { return std::result::Result::Ok(crate::SolanaNodeHealth::Healthy); } return invalid_canary_response("getHealth", "result must be exactly the string ok"); } /// Executes the typed `getGenesisHash` foundation canary. pub async fn get_genesis_hash(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { let method_result = canary_descriptor("getGenesisHash"); let method = match method_result { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = self.execute_standard_rpc(role, method, std::vec::Vec::new()).await; let value = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let hash = match value.as_str() { std::option::Option::Some(hash) => hash, std::option::Option::None => return invalid_canary_response("getGenesisHash", "result must be a string"), }; if hash.is_empty() || hash.trim() != hash { return invalid_canary_response("getGenesisHash", "result must be a non-empty trimmed string"); } return std::result::Result::Ok(crate::SolanaGenesisHash { value: hash.to_owned() }); } /// Executes the typed `getVersion` foundation canary. pub async fn get_version(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result { let method_result = canary_descriptor("getVersion"); let method = match method_result { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = self.execute_standard_rpc(role, method, std::vec::Vec::new()).await; let value = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let decode_result = serde_json::from_value::(value); let decoded = match decode_result { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(error) => return invalid_canary_decode("getVersion", error), }; if decoded.solana_core.is_empty() || decoded.solana_core.trim() != decoded.solana_core { return invalid_canary_response("getVersion", "solana-core must be a non-empty trimmed string"); } return std::result::Result::Ok(crate::SolanaNodeVersion { solana_core: decoded.solana_core, feature_set: decoded.feature_set }); } /// Executes the typed `getBalance` foundation canary. pub async fn get_balance( &self, role: &crate::HttpRoleName, account: &ksp_core_lib::Pubkey, config: std::option::Option<&crate::GetBalanceConfig>, ) -> ksp_core_lib::Result { let method_result = canary_descriptor("getBalance"); let method = match method_result { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut params = std::vec![serde_json::Value::String(account.to_string())]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push(config.to_json_value()); } let result = self.execute_standard_rpc(role, method, params).await; let value = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let decode_result = serde_json::from_value::(value); let decoded = match decode_result { std::result::Result::Ok(decoded) => decoded, std::result::Result::Err(error) => return invalid_canary_decode("getBalance", error), }; let context = crate::SolanaRpcContext::decode_wire("getBalance", decoded.context); let context = match context { std::result::Result::Ok(context) => context, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(crate::GetBalanceResult { context, value: decoded.value }); } } #[derive(serde::Deserialize)] struct WireNodeVersion { #[serde(rename = "solana-core")] solana_core: std::string::String, #[serde(rename = "feature-set", default)] feature_set: std::option::Option, } #[derive(serde::Deserialize)] struct WireBalanceResult { context: serde_json::Value, value: u64, } fn canary_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { let descriptor = crate::find_http_rpc_method(method); return match descriptor { std::option::Option::Some(descriptor) => std::result::Result::Ok(descriptor), std::option::Option::None => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed canary descriptor is missing from the audited registry") .with_context("rpc_method", method), ), }; } fn invalid_canary_decode(method: &str, error: serde_json::Error) -> ksp_core_lib::Result { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP canary response has an invalid shape") .with_context("rpc_method", method) .with_source(error), ); } fn invalid_canary_response(method: &str, message: &str) -> ksp_core_lib::Result { return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method)); } #[cfg(test)] #[path = "../unit_tests/rpc_canary.rs"] mod tests;