0.4.1
This commit is contained in:
@@ -12,3 +12,4 @@
|
|||||||
0.3.4 - Ajout de la fenêtre Demo Ws dans kb_app pour tester les souscriptions live
|
0.3.4 - Ajout de la fenêtre Demo Ws dans kb_app pour tester les souscriptions live
|
||||||
0.3.5 - Stabilisation de Demo Ws, lecture correcte des endpoints activés depuis la config, limitation/throttling de l’affichage UI sous fort débit
|
0.3.5 - Stabilisation de Demo Ws, lecture correcte des endpoints activés depuis la config, limitation/throttling de l’affichage UI sous fort débit
|
||||||
0.4.0 - Socle HttpClient générique async clonable, JSON-RPC HTTP 2.0, résolution d’URL avec api_key_env_var, limiteur local req/sec + burst, helpers initiaux getHealth/getVersion/getSlot
|
0.4.0 - Socle HttpClient générique async clonable, JSON-RPC HTTP 2.0, résolution d’URL avec api_key_env_var, limiteur local req/sec + burst, helpers initiaux getHealth/getVersion/getSlot
|
||||||
|
0.4.1 - Ajout des premiers helpers HTTP Solana haut niveau, dans la continuité de l’API du client WebSocket
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bobobot"
|
||||||
|
|||||||
@@ -5,12 +5,9 @@
|
|||||||
//! This module provides a reusable `HttpClient` built on top of `reqwest` for
|
//! This module provides a reusable `HttpClient` built on top of `reqwest` for
|
||||||
//! Solana RPC HTTP endpoints.
|
//! Solana RPC HTTP endpoints.
|
||||||
//!
|
//!
|
||||||
//! Version `0.4.0` keeps the API intentionally small:
|
//! Version `0.4.1` extends the `0.4.0` transport layer with:
|
||||||
//! - reusable async client
|
//! - raw Solana HTTP helpers
|
||||||
//! - endpoint URL resolution via config
|
//! - typed Solana HTTP helpers using `solana_rpc_client_api` types
|
||||||
//! - local req/sec + burst limiter
|
|
||||||
//! - generic JSON-RPC 2.0 request/response handling
|
|
||||||
//! - a few initial Solana validation helpers
|
|
||||||
|
|
||||||
/// JSON-RPC 2.0 request envelope for HTTP.
|
/// JSON-RPC 2.0 request envelope for HTTP.
|
||||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -291,6 +288,20 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes one JSON-RPC request and returns only the raw `result` field.
|
||||||
|
pub async fn execute_json_rpc_result_raw(
|
||||||
|
&self,
|
||||||
|
method: std::string::String,
|
||||||
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let raw_result = self.execute_json_rpc_request_raw(method, params).await;
|
||||||
|
let raw_response = match raw_result {
|
||||||
|
Ok(raw_response) => raw_response,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
Ok(raw_response.result)
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes one JSON-RPC request and decodes `result` into `T`.
|
/// Executes one JSON-RPC request and decodes `result` into `T`.
|
||||||
pub async fn execute_json_rpc_request_typed<T>(
|
pub async fn execute_json_rpc_request_typed<T>(
|
||||||
&self,
|
&self,
|
||||||
@@ -300,12 +311,12 @@ impl HttpClient {
|
|||||||
where
|
where
|
||||||
T: serde::de::DeserializeOwned,
|
T: serde::de::DeserializeOwned,
|
||||||
{
|
{
|
||||||
let raw_result = self.execute_json_rpc_request_raw(method, params).await;
|
let raw_result = self.execute_json_rpc_result_raw(method, params).await;
|
||||||
let raw_response = match raw_result {
|
let raw_value = match raw_result {
|
||||||
Ok(raw_response) => raw_response,
|
Ok(raw_value) => raw_value,
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
let typed_result = serde_json::from_value::<T>(raw_response.result);
|
let typed_result = serde_json::from_value::<T>(raw_value);
|
||||||
match typed_result {
|
match typed_result {
|
||||||
Ok(value) => Ok(value),
|
Ok(value) => Ok(value),
|
||||||
Err(error) => Err(crate::KbError::Json(format!(
|
Err(error) => Err(crate::KbError::Json(format!(
|
||||||
@@ -314,46 +325,259 @@ impl HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `getHealth`.
|
/// Raw helper for `getHealth`.
|
||||||
pub async fn get_health(&self) -> Result<serde_json::Value, crate::KbError> {
|
pub async fn get_health_raw(&self) -> Result<serde_json::Value, crate::KbError> {
|
||||||
let raw_result = self
|
self.execute_json_rpc_result_raw("getHealth".to_string(), std::vec::Vec::new())
|
||||||
.execute_json_rpc_request_raw("getHealth".to_string(), std::vec::Vec::new())
|
.await
|
||||||
.await;
|
|
||||||
match raw_result {
|
|
||||||
Ok(response) => Ok(response.result),
|
|
||||||
Err(error) => Err(error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `getVersion`.
|
/// Typed helper for `getHealth`.
|
||||||
pub async fn get_version(&self) -> Result<serde_json::Value, crate::KbError> {
|
pub async fn get_health(&self) -> Result<std::string::String, crate::KbError> {
|
||||||
let raw_result = self
|
self.execute_json_rpc_request_typed::<std::string::String>(
|
||||||
.execute_json_rpc_request_raw("getVersion".to_string(), std::vec::Vec::new())
|
"getHealth".to_string(),
|
||||||
.await;
|
std::vec::Vec::new(),
|
||||||
match raw_result {
|
)
|
||||||
Ok(response) => Ok(response.result),
|
.await
|
||||||
Err(error) => Err(error),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calls `getSlot`.
|
/// Raw helper for `getVersion`.
|
||||||
|
pub async fn get_version_raw(&self) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
self.execute_json_rpc_result_raw("getVersion".to_string(), std::vec::Vec::new())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getVersion`.
|
||||||
|
pub async fn get_version(
|
||||||
|
&self,
|
||||||
|
) -> Result<solana_rpc_client_api::response::RpcVersionInfo, crate::KbError> {
|
||||||
|
self.execute_json_rpc_request_typed::<solana_rpc_client_api::response::RpcVersionInfo>(
|
||||||
|
"getVersion".to_string(),
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getSlot`.
|
||||||
|
pub async fn get_slot_raw(
|
||||||
|
&self,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_optional_config_only_params(config);
|
||||||
|
self.execute_json_rpc_result_raw("getSlot".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getSlot`.
|
||||||
pub async fn get_slot(
|
pub async fn get_slot(
|
||||||
&self,
|
&self,
|
||||||
commitment: std::option::Option<std::string::String>,
|
config: std::option::Option<solana_rpc_client_api::config::RpcContextConfig>,
|
||||||
|
) -> Result<u64, crate::KbError> {
|
||||||
|
let config_value_result = kb_serialize_optional_json_value(config, "getSlot config");
|
||||||
|
let config_value = match config_value_result {
|
||||||
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_optional_config_only_params(config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<u64>("getSlot".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getBlockHeight`.
|
||||||
|
pub async fn get_block_height_raw(
|
||||||
|
&self,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
) -> Result<serde_json::Value, crate::KbError> {
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
let mut params = std::vec::Vec::new();
|
let params = kb_build_optional_config_only_params(config);
|
||||||
if let Some(commitment) = commitment {
|
self.execute_json_rpc_result_raw("getBlockHeight".to_string(), params)
|
||||||
params.push(serde_json::json!({
|
.await
|
||||||
"commitment": commitment
|
}
|
||||||
}));
|
|
||||||
}
|
/// Typed helper for `getBlockHeight`.
|
||||||
let raw_result = self
|
pub async fn get_block_height(
|
||||||
.execute_json_rpc_request_raw("getSlot".to_string(), params)
|
&self,
|
||||||
.await;
|
config: std::option::Option<solana_rpc_client_api::config::RpcContextConfig>,
|
||||||
match raw_result {
|
) -> Result<u64, crate::KbError> {
|
||||||
Ok(response) => Ok(response.result),
|
let config_value_result = kb_serialize_optional_json_value(config, "getBlockHeight config");
|
||||||
Err(error) => Err(error),
|
let config_value = match config_value_result {
|
||||||
}
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_optional_config_only_params(config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<u64>("getBlockHeight".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getLatestBlockhash`.
|
||||||
|
pub async fn get_latest_blockhash_raw(
|
||||||
|
&self,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_optional_config_only_params(config);
|
||||||
|
self.execute_json_rpc_result_raw("getLatestBlockhash".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getLatestBlockhash`.
|
||||||
|
pub async fn get_latest_blockhash(
|
||||||
|
&self,
|
||||||
|
config: std::option::Option<solana_rpc_client_api::config::RpcContextConfig>,
|
||||||
|
) -> Result<
|
||||||
|
solana_rpc_client_api::response::Response<solana_rpc_client_api::response::RpcBlockhash>,
|
||||||
|
crate::KbError,
|
||||||
|
> {
|
||||||
|
let config_value_result =
|
||||||
|
kb_serialize_optional_json_value(config, "getLatestBlockhash config");
|
||||||
|
let config_value = match config_value_result {
|
||||||
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_optional_config_only_params(config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<
|
||||||
|
solana_rpc_client_api::response::Response<
|
||||||
|
solana_rpc_client_api::response::RpcBlockhash,
|
||||||
|
>,
|
||||||
|
>("getLatestBlockhash".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getBalance`.
|
||||||
|
pub async fn get_balance_raw(
|
||||||
|
&self,
|
||||||
|
address: std::string::String,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_first_string_optional_config_params(address, config);
|
||||||
|
self.execute_json_rpc_result_raw("getBalance".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getBalance`.
|
||||||
|
pub async fn get_balance(
|
||||||
|
&self,
|
||||||
|
address: std::string::String,
|
||||||
|
config: std::option::Option<solana_rpc_client_api::config::RpcContextConfig>,
|
||||||
|
) -> Result<solana_rpc_client_api::response::Response<u64>, crate::KbError> {
|
||||||
|
let config_value_result = kb_serialize_optional_json_value(config, "getBalance config");
|
||||||
|
let config_value = match config_value_result {
|
||||||
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_first_string_optional_config_params(address, config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<solana_rpc_client_api::response::Response<u64>>(
|
||||||
|
"getBalance".to_string(),
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getAccountInfo`.
|
||||||
|
pub async fn get_account_info_raw(
|
||||||
|
&self,
|
||||||
|
address: std::string::String,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_first_string_optional_config_params(address, config);
|
||||||
|
self.execute_json_rpc_result_raw("getAccountInfo".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getAccountInfo`.
|
||||||
|
pub async fn get_account_info(
|
||||||
|
&self,
|
||||||
|
address: std::string::String,
|
||||||
|
config: std::option::Option<solana_rpc_client_api::config::RpcAccountInfoConfig>,
|
||||||
|
) -> Result<
|
||||||
|
solana_rpc_client_api::response::Response<
|
||||||
|
std::option::Option<solana_rpc_client_api::response::UiAccount>,
|
||||||
|
>,
|
||||||
|
crate::KbError,
|
||||||
|
> {
|
||||||
|
let config_value_result = kb_serialize_optional_json_value(config, "getAccountInfo config");
|
||||||
|
let config_value = match config_value_result {
|
||||||
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_first_string_optional_config_params(address, config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<solana_rpc_client_api::response::Response<
|
||||||
|
std::option::Option<solana_rpc_client_api::response::UiAccount>,
|
||||||
|
>>("getAccountInfo".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getProgramAccounts`.
|
||||||
|
pub async fn get_program_accounts_raw(
|
||||||
|
&self,
|
||||||
|
program_id: std::string::String,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_first_string_optional_config_params(program_id, config);
|
||||||
|
self.execute_json_rpc_result_raw("getProgramAccounts".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getSignaturesForAddress`.
|
||||||
|
pub async fn get_signatures_for_address_raw(
|
||||||
|
&self,
|
||||||
|
address: std::string::String,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_first_string_optional_config_params(address, config);
|
||||||
|
self.execute_json_rpc_result_raw("getSignaturesForAddress".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getSignaturesForAddress`.
|
||||||
|
pub async fn get_signatures_for_address(
|
||||||
|
&self,
|
||||||
|
address: std::string::String,
|
||||||
|
config: std::option::Option<solana_rpc_client_api::config::RpcSignaturesForAddressConfig>,
|
||||||
|
) -> Result<
|
||||||
|
std::vec::Vec<solana_rpc_client_api::response::RpcConfirmedTransactionStatusWithSignature>,
|
||||||
|
crate::KbError,
|
||||||
|
> {
|
||||||
|
let config_value_result =
|
||||||
|
kb_serialize_optional_json_value(config, "getSignaturesForAddress config");
|
||||||
|
let config_value = match config_value_result {
|
||||||
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_first_string_optional_config_params(address, config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<std::vec::Vec<
|
||||||
|
solana_rpc_client_api::response::RpcConfirmedTransactionStatusWithSignature,
|
||||||
|
>>("getSignaturesForAddress".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw helper for `getTransaction`.
|
||||||
|
pub async fn get_transaction_raw(
|
||||||
|
&self,
|
||||||
|
signature: std::string::String,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value, crate::KbError> {
|
||||||
|
let params = kb_build_first_string_optional_config_params(signature, config);
|
||||||
|
self.execute_json_rpc_result_raw("getTransaction".to_string(), params)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed helper for `getTransaction`.
|
||||||
|
pub async fn get_transaction(
|
||||||
|
&self,
|
||||||
|
signature: std::string::String,
|
||||||
|
config: std::option::Option<solana_rpc_client_api::config::RpcTransactionConfig>,
|
||||||
|
) -> Result<
|
||||||
|
std::option::Option<solana_rpc_client_api::response::EncodedTransactionWithStatusMeta>,
|
||||||
|
crate::KbError,
|
||||||
|
> {
|
||||||
|
let config_value_result = kb_serialize_optional_json_value(config, "getTransaction config");
|
||||||
|
let config_value = match config_value_result {
|
||||||
|
Ok(config_value) => config_value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let params = kb_build_first_string_optional_config_params(signature, config_value);
|
||||||
|
self.execute_json_rpc_request_typed::<
|
||||||
|
std::option::Option<solana_rpc_client_api::response::EncodedTransactionWithStatusMeta>,
|
||||||
|
>("getTransaction".to_string(), params)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn acquire_rate_limit_slot(&self) -> Result<(), crate::KbError> {
|
async fn acquire_rate_limit_slot(&self) -> Result<(), crate::KbError> {
|
||||||
@@ -420,7 +644,6 @@ pub fn parse_kb_json_rpc_http_response_value(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let jsonrpc_value_option = object.get("jsonrpc");
|
let jsonrpc_value_option = object.get("jsonrpc");
|
||||||
let jsonrpc_value = match jsonrpc_value_option {
|
let jsonrpc_value = match jsonrpc_value_option {
|
||||||
Some(jsonrpc_value) => jsonrpc_value,
|
Some(jsonrpc_value) => jsonrpc_value,
|
||||||
@@ -430,7 +653,6 @@ pub fn parse_kb_json_rpc_http_response_value(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let jsonrpc_string_option = jsonrpc_value.as_str();
|
let jsonrpc_string_option = jsonrpc_value.as_str();
|
||||||
let jsonrpc_string = match jsonrpc_string_option {
|
let jsonrpc_string = match jsonrpc_string_option {
|
||||||
Some(jsonrpc_string) => jsonrpc_string,
|
Some(jsonrpc_string) => jsonrpc_string,
|
||||||
@@ -440,18 +662,15 @@ pub fn parse_kb_json_rpc_http_response_value(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if jsonrpc_string != "2.0" {
|
if jsonrpc_string != "2.0" {
|
||||||
return Err(crate::KbError::Json(format!(
|
return Err(crate::KbError::Json(format!(
|
||||||
"unsupported http json-rpc version '{}'",
|
"unsupported http json-rpc version '{}'",
|
||||||
jsonrpc_string
|
jsonrpc_string
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_result = object.contains_key("result");
|
let has_result = object.contains_key("result");
|
||||||
let has_error = object.contains_key("error");
|
let has_error = object.contains_key("error");
|
||||||
let has_id = object.contains_key("id");
|
let has_id = object.contains_key("id");
|
||||||
|
|
||||||
if has_id && has_result && !has_error {
|
if has_id && has_result && !has_error {
|
||||||
let response_result = serde_json::from_value::<KbJsonRpcHttpSuccessResponse>(value.clone());
|
let response_result = serde_json::from_value::<KbJsonRpcHttpSuccessResponse>(value.clone());
|
||||||
return match response_result {
|
return match response_result {
|
||||||
@@ -461,7 +680,6 @@ pub fn parse_kb_json_rpc_http_response_value(
|
|||||||
))),
|
))),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_id && has_error && !has_result {
|
if has_id && has_error && !has_result {
|
||||||
let response_result = serde_json::from_value::<KbJsonRpcHttpErrorResponse>(value.clone());
|
let response_result = serde_json::from_value::<KbJsonRpcHttpErrorResponse>(value.clone());
|
||||||
return match response_result {
|
return match response_result {
|
||||||
@@ -476,6 +694,49 @@ pub fn parse_kb_json_rpc_http_response_value(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kb_build_optional_config_only_params(
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> std::vec::Vec<serde_json::Value> {
|
||||||
|
let mut params = std::vec::Vec::new();
|
||||||
|
if let Some(config) = config {
|
||||||
|
params.push(config);
|
||||||
|
}
|
||||||
|
params
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kb_build_first_string_optional_config_params(
|
||||||
|
first: std::string::String,
|
||||||
|
config: std::option::Option<serde_json::Value>,
|
||||||
|
) -> std::vec::Vec<serde_json::Value> {
|
||||||
|
let mut params = vec![serde_json::Value::String(first)];
|
||||||
|
if let Some(config) = config {
|
||||||
|
params.push(config);
|
||||||
|
}
|
||||||
|
params
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kb_serialize_optional_json_value<T>(
|
||||||
|
value: std::option::Option<T>,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<std::option::Option<serde_json::Value>, crate::KbError>
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
Some(value) => {
|
||||||
|
let value_result = serde_json::to_value(value);
|
||||||
|
match value_result {
|
||||||
|
Ok(value) => Ok(Some(value)),
|
||||||
|
Err(error) => Err(crate::KbError::Json(format!(
|
||||||
|
"cannot serialize {}: {error}",
|
||||||
|
label
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn kb_http_shorten_text(input: &str, max_chars: usize) -> std::string::String {
|
fn kb_http_shorten_text(input: &str, max_chars: usize) -> std::string::String {
|
||||||
let char_count = input.chars().count();
|
let char_count = input.chars().count();
|
||||||
if char_count <= max_chars {
|
if char_count <= max_chars {
|
||||||
@@ -517,7 +778,7 @@ mod tests {
|
|||||||
let (mut stream, _peer_addr) = accept_result.expect("accept must succeed");
|
let (mut stream, _peer_addr) = accept_result.expect("accept must succeed");
|
||||||
let observed_methods_for_connection = observed_methods_for_server.clone();
|
let observed_methods_for_connection = observed_methods_for_server.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut buffer = vec![0u8; 8192];
|
let mut buffer = vec![0u8; 65536];
|
||||||
let read_result = stream.read(&mut buffer).await;
|
let read_result = stream.read(&mut buffer).await;
|
||||||
let bytes_read = read_result.expect("read must succeed");
|
let bytes_read = read_result.expect("read must succeed");
|
||||||
let request_text =
|
let request_text =
|
||||||
@@ -562,6 +823,82 @@ mod tests {
|
|||||||
"result": 424242u64,
|
"result": 424242u64,
|
||||||
"id": id
|
"id": id
|
||||||
}).to_string()
|
}).to_string()
|
||||||
|
} else if method == "getBlockHeight" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": 919191u64,
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
|
} else if method == "getLatestBlockhash" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"context": {
|
||||||
|
"slot": 999u64
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"blockhash": "11111111111111111111111111111111",
|
||||||
|
"lastValidBlockHeight": 12345u64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
|
} else if method == "getBalance" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"context": {
|
||||||
|
"slot": 77u64
|
||||||
|
},
|
||||||
|
"value": 5000u64
|
||||||
|
},
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
|
} else if method == "getSignaturesForAddress" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"signature": "5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7",
|
||||||
|
"slot": 88u64,
|
||||||
|
"err": null,
|
||||||
|
"memo": null,
|
||||||
|
"blockTime": 1700000000i64,
|
||||||
|
"confirmationStatus": "finalized"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
|
} else if method == "getTransaction" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": null,
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
|
} else if method == "getAccountInfo" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": {
|
||||||
|
"context": {
|
||||||
|
"slot": 55u64
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"data": ["", "base64"],
|
||||||
|
"executable": false,
|
||||||
|
"lamports": 1u64,
|
||||||
|
"owner": "11111111111111111111111111111111",
|
||||||
|
"rentEpoch": 0u64,
|
||||||
|
"space": 0u64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
|
} else if method == "getProgramAccounts" {
|
||||||
|
serde_json::json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"result": [],
|
||||||
|
"id": id
|
||||||
|
}).to_string()
|
||||||
} else {
|
} else {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -664,48 +1001,165 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_health_works() {
|
async fn typed_helpers_work_for_basic_methods() {
|
||||||
let server = TestHttpServer::spawn().await;
|
let server = TestHttpServer::spawn().await;
|
||||||
let endpoint = make_http_endpoint(server.url.clone());
|
let endpoint = make_http_endpoint(server.url.clone());
|
||||||
let client = crate::HttpClient::new(endpoint).expect("client creation must succeed");
|
let client = crate::HttpClient::new(endpoint).expect("client creation must succeed");
|
||||||
let result = client.get_health().await.expect("get_health must succeed");
|
let health = client.get_health().await.expect("get_health must succeed");
|
||||||
assert_eq!(result, serde_json::Value::String("ok".to_string()));
|
assert_eq!(health, "ok".to_string());
|
||||||
let observed_methods = server.observed_methods_snapshot().await;
|
let version = client
|
||||||
assert!(observed_methods.iter().any(|method| method == "getHealth"));
|
|
||||||
server.shutdown().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_version_works() {
|
|
||||||
let server = TestHttpServer::spawn().await;
|
|
||||||
let endpoint = make_http_endpoint(server.url.clone());
|
|
||||||
let client = crate::HttpClient::new(endpoint).expect("client creation must succeed");
|
|
||||||
let result = client
|
|
||||||
.get_version()
|
.get_version()
|
||||||
.await
|
.await
|
||||||
.expect("get_version must succeed");
|
.expect("get_version must succeed");
|
||||||
|
assert_eq!(version.solana_core, "2.2.3".to_string());
|
||||||
|
let slot = client.get_slot(None).await.expect("get_slot must succeed");
|
||||||
|
assert_eq!(slot, 424242u64);
|
||||||
|
let block_height = client
|
||||||
|
.get_block_height(None)
|
||||||
|
.await
|
||||||
|
.expect("get_block_height must succeed");
|
||||||
|
assert_eq!(block_height, 919191u64);
|
||||||
|
let latest_blockhash = client
|
||||||
|
.get_latest_blockhash(None)
|
||||||
|
.await
|
||||||
|
.expect("get_latest_blockhash must succeed");
|
||||||
|
assert_eq!(latest_blockhash.context.slot, 999u64);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result["solana-core"],
|
latest_blockhash.value.blockhash,
|
||||||
serde_json::Value::String("2.2.3".to_string())
|
"11111111111111111111111111111111".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(result["feature-set"], serde_json::Value::from(123u64));
|
assert_eq!(latest_blockhash.value.last_valid_block_height, 12345u64);
|
||||||
|
let balance = client
|
||||||
|
.get_balance("11111111111111111111111111111111".to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("get_balance must succeed");
|
||||||
|
assert_eq!(balance.context.slot, 77u64);
|
||||||
|
assert_eq!(balance.value, 5000u64);
|
||||||
|
let signatures = client
|
||||||
|
.get_signatures_for_address("11111111111111111111111111111111".to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("get_signatures_for_address must succeed");
|
||||||
|
assert_eq!(signatures.len(), 1);
|
||||||
|
assert_eq!(signatures[0].slot, 88u64);
|
||||||
|
let transaction = client
|
||||||
|
.get_transaction(
|
||||||
|
"5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("get_transaction must succeed");
|
||||||
|
assert!(transaction.is_none());
|
||||||
let observed_methods = server.observed_methods_snapshot().await;
|
let observed_methods = server.observed_methods_snapshot().await;
|
||||||
|
assert!(observed_methods.iter().any(|method| method == "getHealth"));
|
||||||
assert!(observed_methods.iter().any(|method| method == "getVersion"));
|
assert!(observed_methods.iter().any(|method| method == "getVersion"));
|
||||||
|
assert!(observed_methods.iter().any(|method| method == "getSlot"));
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getBlockHeight")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getLatestBlockhash")
|
||||||
|
);
|
||||||
|
assert!(observed_methods.iter().any(|method| method == "getBalance"));
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getSignaturesForAddress")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getTransaction")
|
||||||
|
);
|
||||||
server.shutdown().await;
|
server.shutdown().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_slot_works() {
|
async fn raw_helpers_send_expected_methods() {
|
||||||
let server = TestHttpServer::spawn().await;
|
let server = TestHttpServer::spawn().await;
|
||||||
let endpoint = make_http_endpoint(server.url.clone());
|
let endpoint = make_http_endpoint(server.url.clone());
|
||||||
let client = crate::HttpClient::new(endpoint).expect("client creation must succeed");
|
let client = crate::HttpClient::new(endpoint).expect("client creation must succeed");
|
||||||
let result = client
|
let _ = client
|
||||||
.get_slot(Some("finalized".to_string()))
|
.get_health_raw()
|
||||||
.await
|
.await
|
||||||
.expect("get_slot must succeed");
|
.expect("get_health_raw must succeed");
|
||||||
assert_eq!(result, serde_json::Value::from(424242u64));
|
let _ = client
|
||||||
|
.get_version_raw()
|
||||||
|
.await
|
||||||
|
.expect("get_version_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_slot_raw(None)
|
||||||
|
.await
|
||||||
|
.expect("get_slot_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_block_height_raw(None)
|
||||||
|
.await
|
||||||
|
.expect("get_block_height_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_latest_blockhash_raw(None)
|
||||||
|
.await
|
||||||
|
.expect("get_latest_blockhash_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_balance_raw("11111111111111111111111111111111".to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("get_balance_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_account_info_raw("11111111111111111111111111111111".to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("get_account_info_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_program_accounts_raw("11111111111111111111111111111111".to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("get_program_accounts_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_signatures_for_address_raw("11111111111111111111111111111111".to_string(), None)
|
||||||
|
.await
|
||||||
|
.expect("get_signatures_for_address_raw must succeed");
|
||||||
|
let _ = client
|
||||||
|
.get_transaction_raw(
|
||||||
|
"5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("get_transaction_raw must succeed");
|
||||||
let observed_methods = server.observed_methods_snapshot().await;
|
let observed_methods = server.observed_methods_snapshot().await;
|
||||||
|
assert!(observed_methods.iter().any(|method| method == "getHealth"));
|
||||||
|
assert!(observed_methods.iter().any(|method| method == "getVersion"));
|
||||||
assert!(observed_methods.iter().any(|method| method == "getSlot"));
|
assert!(observed_methods.iter().any(|method| method == "getSlot"));
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getBlockHeight")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getLatestBlockhash")
|
||||||
|
);
|
||||||
|
assert!(observed_methods.iter().any(|method| method == "getBalance"));
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getAccountInfo")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getProgramAccounts")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getSignaturesForAddress")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
observed_methods
|
||||||
|
.iter()
|
||||||
|
.any(|method| method == "getTransaction")
|
||||||
|
);
|
||||||
server.shutdown().await;
|
server.shutdown().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user