v0.2.1-pre.005
This commit is contained in:
301
crates/ksp-onchain-transport-lib/src/rpc_canary.rs
Normal file
301
crates/ksp-onchain-transport-lib/src/rpc_canary.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_canary.rs
|
||||
// version: 1
|
||||
|
||||
/// Commitment level accepted by the initial typed Solana HTTP canary adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
Processed,
|
||||
/// Query a bank confirmed by cluster vote.
|
||||
Confirmed,
|
||||
/// Query a finalized bank.
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl SolanaCommitment {
|
||||
/// Returns the Solana JSON-RPC commitment string.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Processed => "processed",
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Finalized => "finalized",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional typed configuration for `getBalance`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct GetBalanceConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl GetBalanceConfig {
|
||||
/// Creates an explicit `getBalance` configuration.
|
||||
#[must_use]
|
||||
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>, min_context_slot: std::option::Option<u64>) -> Self {
|
||||
return Self { commitment, min_context_slot };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns the optional minimum context slot.
|
||||
#[must_use]
|
||||
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
|
||||
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 {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
|
||||
object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<u32>,
|
||||
}
|
||||
|
||||
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<u32> {
|
||||
return self.feature_set;
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed Solana RPC context used by the initial account canary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaRpcContext {
|
||||
slot: u64,
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaRpcContext {
|
||||
/// Returns the context slot reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn slot(&self) -> u64 {
|
||||
return self.slot;
|
||||
}
|
||||
|
||||
/// Returns the optional RPC API version reported by the node.
|
||||
#[must_use]
|
||||
pub fn api_version(&self) -> std::option::Option<&str> {
|
||||
return match self.api_version.as_ref() {
|
||||
std::option::Option::Some(value) => std::option::Option::Some(value.as_str()),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<crate::SolanaNodeHealth> {
|
||||
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<crate::SolanaGenesisHash> {
|
||||
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<crate::SolanaNodeVersion> {
|
||||
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::<WireNodeVersion>(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<crate::GetBalanceResult> {
|
||||
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::<WireBalanceResult>(value);
|
||||
let decoded = match decode_result {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(error) => return invalid_canary_decode("getBalance", error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::GetBalanceResult {
|
||||
context: crate::SolanaRpcContext { slot: decoded.context.slot, api_version: decoded.context.api_version },
|
||||
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<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireBalanceResult {
|
||||
context: WireRpcContext,
|
||||
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<T>(method: &str, error: serde_json::Error) -> ksp_core_lib::Result<T> {
|
||||
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<T>(method: &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("rpc_method", method));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_canary.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user