v0.1.0-pre.024

This commit is contained in:
2026-07-25 00:48:29 +02:00
parent c30ba8a2d9
commit f5648ff138
22 changed files with 7554 additions and 11 deletions

View File

@@ -3,6 +3,13 @@
# CHANGELOG
## 0.1.0-pre.024
- Première tranche fonctionnelle de `kb-onchain-transport`.
- Migration des contrats JSON-RPC, des rôles dendpoints, des clients et pools HTTP, des validateurs et des méthodes HTTP Solana standard.
- Migration des adaptateurs RPC dexécution communs vers les types consolidés de `kb-lib`.
- WebSocket, acquisition canonique de transactions et soumission/confirmation réseau restent planifiés pour les tranches suivantes.
## 0.1.0-pre.023
- Renommage structurel de `kb-rpc` en `kb-onchain-transport`.

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 16 -->
<!-- version: 17 -->
# ROADMAP — khadhroony-bot3
@@ -17,6 +17,12 @@
- [x] Porter leurs matérialisateurs déjà implémentés dans bot2.
- [ ] Porter leurs exécuteurs.
- [x] Fusionner `kb_store_core` et `kb_store_pg` dans `kb-store`.
- [ ] Migrer `kb-onchain-transport` depuis lancienne `kb_rpc`.
- [x] Renommer structurellement la crate.
- [x] Porter les contrats JSON-RPC, rôles dendpoints, validation, clients/pools HTTP et méthodes HTTP standard.
- [ ] Porter WebSocket, sessions et pools dabonnements.
- [ ] Porter lacquisition canonique `getTransaction` et `getSignaturesForAddress`.
- [ ] Porter simulation, envoi et confirmation réseau complets.
- [ ] Adapter `kb-pipeline` aux nouveaux chemins publics.
- [ ] Adapter `kb-app-demo` et rétablir les validations fonctionnelles.
- [ ] Créer ultérieurement une crate off-chain dédiée au fetch borné HTTP/IPFS/Arweave des URI de metadata, séparée des décodeurs, matérialisateurs et du replay canonique on-chain.

View File

@@ -45,14 +45,14 @@
"kb-lib.materializer.transaction",
"kb-store",
"kb-onchain-transport",
"kb_wallet",
"kb_pipeline",
"kb_app_demo",
"kb_executor_metadata_metaplex_token_metadata",
"kb_executor_metadata_spl_name_service",
"kb_executor_spl_account_compression",
"kb_executor_spl_noop",
"kb_executor_spl_single_pool",
"kb_pipeline",
"kb_wallet"
"kb_executor_spl_single_pool"
]
},
{

View File

@@ -1,5 +1,5 @@
# file: kb-onchain-transport/Cargo.toml
# version: 2
# version: 4
[package]
name = "kb-onchain-transport"
@@ -9,7 +9,16 @@ license.workspace = true
publish.workspace = true
[dependencies]
base64.workspace = true
bs58.workspace = true
kb-config = { path = "../kb-config" }
kb-core = { path = "../kb-core" }
kb-lib = { path = "../kb-lib" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,22 @@
// file: kb-onchain-transport/src/client.rs
// version: 3
//! RPC client scaffold for Solana ingestion.
/// RPC endpoint configuration.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RpcEndpoint {
/// HTTP RPC URL.
pub http_url: std::string::String,
/// Optional WebSocket RPC URL.
pub ws_url: std::option::Option<std::string::String>,
}
/// Minimal Solana RPC client abstraction.
pub trait SolanaRpcClient {
/// Fetches a raw transaction payload by signature.
fn get_transaction_raw_json(
&self,
signature: &kb_lib::MdSignature,
) -> kb_core::Result<std::option::Option<std::string::String>>;
}

View File

@@ -0,0 +1,41 @@
// file: kb-onchain-transport/src/constants.rs
// version: 7
//! Local constants for the `kb-onchain-transport` crate.
/// Canonical tracing target for this crate.
pub(crate) const TRACING_TARGET: &str = "kb-onchain-transport";
/// Official Devnet genesis hash.
pub(crate) const DEVNET_GENESIS_HASH: &str = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
/// Official Testnet genesis hash.
pub(crate) const TESTNET_GENESIS_HASH: &str = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY";
/// Official Mainnet genesis hash; RPC and CLI endpoints retain the legacy `mainnet-beta` alias.
pub(crate) const MAINNET_GENESIS_HASH: &str = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
/// Local defensive maximum for one base64 message or transaction request value.
pub(crate) const MAX_EXECUTION_RPC_BASE64_LENGTH: usize = 65_536;
/// Local defensive maximum for decoded account data returned by one execution RPC request.
pub(crate) const MAX_EXECUTION_ACCOUNT_DATA_BYTES: usize = 65_536;
/// Local defensive maximum for account snapshots requested from one simulation.
pub(crate) const MAX_SIMULATION_ACCOUNT_COUNT: usize = 128;
/// Maximum signatures accepted by one `getSignatureStatuses` request.
pub(crate) const MAX_SIGNATURE_STATUS_COUNT: usize = 256;
/// Maximum confirmation polling attempts accepted by the bounded helper.
pub(crate) const MAX_CONFIRMATION_ATTEMPTS: u32 = 10_000;
/// Maximum delay between confirmation polls.
pub(crate) const MAX_CONFIRMATION_POLL_INTERVAL_MS: u64 = 60_000;
/// Maximum number of public keys accepted by `getMultipleAccounts`.
pub(crate) const MAX_MULTIPLE_ACCOUNT_COUNT: usize = 100;
/// Maximum writable-account set accepted by `getRecentPrioritizationFees`.
pub(crate) const MAX_PRIORITIZATION_FEE_ACCOUNT_COUNT: usize = 128;
/// Maximum sample count accepted by `getRecentPerformanceSamples`.
pub(crate) const MAX_PERFORMANCE_SAMPLE_COUNT: usize = 720;
/// Maximum consecutive slot leaders accepted by `getSlotLeaders`.
pub(crate) const MAX_SLOT_LEADER_COUNT: u64 = 5_000;
/// Maximum slot span or result limit accepted by block-range methods.
pub(crate) const MAX_BLOCK_RANGE: u64 = 500_000;
/// Maximum decoded memcmp payload accepted by the standard RPC contract.
pub(crate) const MAX_MEMCMP_DECODED_BYTES: usize = 128;
/// Maximum base58 text length for a 128-byte memcmp payload.
pub(crate) const MAX_MEMCMP_BASE58_LENGTH: usize = 175;
/// Maximum base64 text length for a 128-byte memcmp payload.
pub(crate) const MAX_MEMCMP_BASE64_LENGTH: usize = 172;

View File

@@ -0,0 +1,190 @@
// file: kb-onchain-transport/src/endpoint_role.rs
// version: 3
//! Endpoint role helpers shared by HTTP and WebSocket pools.
/// Snapshot of one endpoint role and its local limits.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EndpointRoleSnapshot {
/// Role code used by endpoint pools.
pub role: std::string::String,
/// Enables this role on the endpoint.
pub enabled: bool,
/// Request or subscription kinds handled by this role.
pub request_kinds: std::vec::Vec<std::string::String>,
/// Role priority where lower values are preferred.
pub priority: u32,
/// Requests per second allowed for this role on this URL.
pub requests_per_second: u32,
/// Burst capacity allowed for this role on this URL.
pub burst_capacity: u32,
/// Maximum concurrent requests allowed for this role on this URL.
pub max_concurrent_requests: u32,
/// Maximum subscriptions allowed for this role on this URL.
pub max_subscriptions: u32,
/// Pause after a rate limit response in milliseconds.
pub pause_after_rate_limit_ms: u64,
}
impl crate::EndpointRoleSnapshot {
/// Builds a serializable snapshot from configuration.
pub fn from_config(config: &kb_config::EndpointRoleConfig) -> Self {
return Self {
role: config.role.clone(),
enabled: config.enabled,
request_kinds: config.request_kinds.clone(),
priority: config.priority,
requests_per_second: config.requests_per_second,
burst_capacity: config.burst_capacity,
max_concurrent_requests: config.max_concurrent_requests,
max_subscriptions: config.max_subscriptions,
pause_after_rate_limit_ms: config.pause_after_rate_limit_ms,
};
}
}
/// Converts a JSON-RPC method name into a stable snake_case request kind.
pub fn request_kind_from_method(method: &str) -> std::string::String {
let trimmed = method.trim();
if trimmed == "logsSubscribe" {
return "logs_subscribe_mentions".to_string();
}
return crate::endpoint_role::camel_or_pascal_to_snake(trimmed);
}
/// Returns true when one endpoint role can handle the requested role and kind.
pub(crate) fn role_matches(
role_config: &kb_config::EndpointRoleConfig,
required_role: &str,
request_kind: &str,
) -> bool {
if !role_config.enabled {
return false;
}
if role_config.role != required_role {
return false;
}
for configured_kind in &role_config.request_kinds {
if configured_kind == request_kind {
return true;
}
if configured_kind == "*" {
return true;
}
}
return false;
}
fn camel_or_pascal_to_snake(value: &str) -> std::string::String {
let mut output = std::string::String::new();
let mut previous_was_lower_or_digit = false;
for character in value.chars() {
if character == '-' || character == ' ' || character == '.' {
if !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
if character.is_ascii_uppercase() {
if previous_was_lower_or_digit && !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
output.push(character.to_ascii_lowercase());
previous_was_lower_or_digit = false;
continue;
}
if character == '_' {
if !output.ends_with('_') && !output.is_empty() {
output.push('_');
}
previous_was_lower_or_digit = false;
continue;
}
output.push(character);
previous_was_lower_or_digit = character.is_ascii_lowercase() || character.is_ascii_digit();
}
return output.trim_matches('_').to_string();
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
enabled: bool,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
#[test]
fn request_kind_converts_camel_case_methods() {
assert_eq!(crate::request_kind_from_method("getLatestBlockhash"), "get_latest_blockhash");
assert_eq!(crate::request_kind_from_method("sendTransaction"), "send_transaction");
}
#[test]
fn request_kind_keeps_existing_snake_case_methods() {
assert_eq!(crate::request_kind_from_method("get_latest_blockhash"), "get_latest_blockhash");
assert_eq!(
crate::request_kind_from_method("logs_subscribe_mentions"),
"logs_subscribe_mentions"
);
}
#[test]
fn request_kind_normalizes_separators() {
assert_eq!(crate::request_kind_from_method("get-Block"), "get_block");
assert_eq!(crate::request_kind_from_method("program.Subscribe"), "program_subscribe");
}
#[test]
fn request_kind_maps_logs_subscribe_to_mentions_role_kind() {
assert_eq!(crate::request_kind_from_method("logsSubscribe"), "logs_subscribe_mentions");
}
#[test]
fn role_matches_exact_request_kind() {
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
assert!(crate::role_matches(&role, "http_queries", "get_version"));
}
#[test]
fn role_matches_wildcard_request_kind() {
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
assert!(crate::role_matches(&role, "http_queries", "get_block"));
}
#[test]
fn role_does_not_match_when_disabled() {
let role = role_config("http_queries", false, std::vec!["*".to_string()]);
assert!(!crate::role_matches(&role, "http_queries", "get_version"));
}
#[test]
fn role_does_not_match_different_role() {
let role = role_config("http_queries", true, std::vec!["*".to_string()]);
assert!(!crate::role_matches(&role, "http_heavy", "get_block"));
}
#[test]
fn endpoint_role_snapshot_preserves_limits() {
let role = role_config("http_queries", true, std::vec!["get_version".to_string()]);
let snapshot = crate::EndpointRoleSnapshot::from_config(&role);
assert_eq!(snapshot.role, "http_queries");
assert_eq!(snapshot.request_kinds, std::vec!["get_version".to_string()]);
assert_eq!(snapshot.requests_per_second, 10);
assert_eq!(snapshot.max_subscriptions, 16);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,383 @@
// file: kb-onchain-transport/src/http_client.rs
// version: 11
//! HTTP JSON-RPC client for standard Solana RPC endpoints.
/// Local HTTP method class used for routing diagnostics.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
pub enum HttpMethodClass {
/// Standard RPC reads and generic methods.
GeneralRpc,
/// Transaction submission methods.
SendTransaction,
/// Resource-intensive read methods.
HeavyRead,
}
/// Snapshot of one pooled HTTP endpoint.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpPoolClientSnapshot {
/// Logical endpoint name.
pub endpoint_name: std::string::String,
/// Provider name.
pub provider: std::string::String,
/// Endpoint URL.
pub endpoint_url: std::string::String,
/// Supported roles.
pub roles: std::vec::Vec<crate::EndpointRoleSnapshot>,
/// Status string.
pub status: std::string::String,
}
/// HTTP JSON-RPC client bound to one configured endpoint.
#[derive(Clone, Debug)]
pub struct HttpClient {
endpoint: kb_config::HttpEndpointConfig,
client: reqwest::Client,
next_request_id: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
impl crate::HttpClient {
/// Creates a new HTTP client bound to one endpoint.
pub fn new(endpoint: kb_config::HttpEndpointConfig) -> kb_core::Result<Self> {
if !endpoint.enabled {
tracing::error!(target: crate::TRACING_TARGET, action = "create_http_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, error_code = "http_endpoint_disabled", "cannot create HTTP client for disabled endpoint");
return std::result::Result::Err(kb_core::Error::config(format!(
"http endpoint '{}' is disabled",
endpoint.name
)));
}
let timeout = std::time::Duration::from_millis(endpoint.request_timeout_ms);
let connect_timeout = std::time::Duration::from_millis(endpoint.connect_timeout_ms);
let client_result = reqwest::Client::builder()
.timeout(timeout)
.connect_timeout(connect_timeout)
.pool_max_idle_per_host(endpoint.max_idle_connections_per_host as usize)
.build();
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "create_http_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, error = %error, "HTTP client construction failed");
return std::result::Result::Err(kb_core::Error::http(format!(
"cannot build http client for endpoint '{}': {error}",
endpoint.name
)));
},
};
tracing::debug!(target: crate::TRACING_TARGET, action = "create_http_client", endpoint_name = %endpoint.name, provider = %endpoint.provider, role_count = endpoint.roles.len(), "HTTP client created");
return std::result::Result::Ok(Self {
endpoint,
client,
next_request_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)),
});
}
/// Returns the endpoint name.
pub fn endpoint_name(&self) -> &str {
return self.endpoint.name.as_str();
}
/// Returns the provider name.
pub fn provider(&self) -> &str {
return self.endpoint.provider.as_str();
}
/// Returns the endpoint URL.
pub fn endpoint_url(&self) -> &str {
return self.endpoint.url.as_str();
}
/// Returns the endpoint configuration.
pub fn endpoint_config(&self) -> &kb_config::HttpEndpointConfig {
return &self.endpoint;
}
/// Returns true when this endpoint supports the required role and request kind.
pub fn can_handle(&self, required_role: &str, request_kind: &str) -> bool {
if !self.endpoint.enabled {
return false;
}
for role in &self.endpoint.roles {
if crate::role_matches(role, required_role, request_kind) {
return true;
}
}
return false;
}
/// Returns a serializable endpoint snapshot.
pub fn snapshot(&self) -> crate::HttpPoolClientSnapshot {
let mut roles = std::vec::Vec::new();
for role in &self.endpoint.roles {
roles.push(crate::EndpointRoleSnapshot::from_config(role));
}
return crate::HttpPoolClientSnapshot {
endpoint_name: self.endpoint.name.clone(),
provider: self.endpoint.provider.clone(),
endpoint_url: self.endpoint.url.clone(),
roles,
status: "active".to_string(),
};
}
/// Classifies a Solana HTTP method into a broad local class.
pub fn classify_method(method: &str) -> crate::HttpMethodClass {
let standard = crate::standard_http_method(method);
return match standard {
std::option::Option::Some(specification) => specification.method_class(),
std::option::Option::None => crate::HttpMethodClass::GeneralRpc,
};
}
/// Executes one typed standard HTTP request and decodes its method-specific result.
pub async fn execute_standard_request<Request>(
&self,
request: &Request,
) -> kb_core::Result<<Request as crate::StandardHttpRequest>::Response>
where
Request: crate::StandardHttpRequest,
{
let specification =
match crate::standard_http_method(<Request as crate::StandardHttpRequest>::METHOD) {
std::option::Option::Some(specification) => specification,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"typed standard request '{}' is absent from the canonical registry",
<Request as crate::StandardHttpRequest>::METHOD
)));
},
};
if specification.contract != crate::StandardRpcContract::TypedAdapter {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"standard request '{}' is not declared as a typed adapter",
<Request as crate::StandardHttpRequest>::METHOD
)));
}
let params = match request.params() {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let raw = match self
.execute_json_rpc_result_raw(
<Request as crate::StandardHttpRequest>::METHOD.to_string(),
params,
)
.await
{
std::result::Result::Ok(raw) => raw,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match serde_json::from_value::<<Request as crate::StandardHttpRequest>::Response>(
raw,
) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(format!(
"cannot decode standard JSON-RPC result for '{}': {error}",
<Request as crate::StandardHttpRequest>::METHOD
)))
},
};
}
/// Executes one explicitly registered standard HTTP method and returns its raw result.
pub async fn execute_standard_method_raw(
&self,
method: &crate::StandardHttpMethodSpec,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
return self.execute_json_rpc_result_raw(method.method.to_string(), params).await;
}
/// Executes one JSON-RPC request and returns the raw result value.
pub async fn execute_json_rpc_result_raw(
&self,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
let request_id = self.next_request_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let parameter_count = params.len();
let method_class = crate::HttpClient::classify_method(method.as_str());
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, method_class = ?method_class, parameter_count, "send HTTP JSON-RPC request");
let request = crate::JsonRpcRequest::new_with_u64_id(request_id, method.clone(), params);
let response_result =
self.client.post(self.endpoint.url.as_str()).json(&request).send().await;
let response = match response_result {
std::result::Result::Ok(response) => response,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, error = %error, "HTTP JSON-RPC transport failed");
return std::result::Result::Err(kb_core::Error::http(format!(
"http json-rpc request '{}' failed on endpoint '{}': {error}",
method, self.endpoint.name
)));
},
};
let status = response.status();
let text_result = response.text().await;
let text = match text_result {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, error = %error, "HTTP JSON-RPC response body read failed");
return std::result::Result::Err(kb_core::Error::http(format!(
"cannot read http json-rpc response '{}' from endpoint '{}': {error}",
method, self.endpoint.name
)));
},
};
if !status.is_success() {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, response_byte_length = text.len(), "HTTP JSON-RPC endpoint returned non-success status");
return std::result::Result::Err(kb_core::Error::http(format!(
"http json-rpc endpoint '{}' returned status {}: {}",
self.endpoint.name, status, text
)));
}
let parsed = match crate::parse_json_rpc_text(&text) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, response_byte_length = text.len(), error = %error, "HTTP JSON-RPC response parsing failed");
return std::result::Result::Err(error);
},
};
return match parsed {
crate::JsonRpcResponse::Success(success) => {
tracing::debug!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, http_status = %status, response_byte_length = text.len(), outcome = "success", "HTTP JSON-RPC request completed");
std::result::Result::Ok(success.result)
},
crate::JsonRpcResponse::Error(error_response) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, rpc_error_code = error_response.error.code, rpc_error_message = %error_response.error.message, "HTTP JSON-RPC endpoint returned an RPC error");
std::result::Result::Err(kb_core::Error::http(format!(
"json-rpc error {} from '{}': {}",
error_response.error.code, self.endpoint.name, error_response.error.message
)))
},
crate::JsonRpcResponse::Notification(_) => {
tracing::error!(target: crate::TRACING_TARGET, action = "execute_http_json_rpc", endpoint_name = %self.endpoint.name, provider = %self.endpoint.provider, request_id, method = %method, "HTTP JSON-RPC response was an unexpected notification");
std::result::Result::Err(kb_core::Error::http(
"http json-rpc response cannot be a notification".to_string(),
))
},
};
}
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled: true,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
fn endpoint(enabled: bool) -> kb_config::HttpEndpointConfig {
return kb_config::HttpEndpointConfig {
name: "http_a".to_string(),
enabled,
provider: "test".to_string(),
cluster: "devnet".to_string(),
url: "https://example.invalid".to_string(),
connect_timeout_ms: 100,
request_timeout_ms: 100,
max_idle_connections_per_host: 2,
roles: std::vec![
role_config("http_queries", std::vec!["get_version".to_string()]),
role_config("http_heavy", std::vec!["get_block".to_string()]),
role_config("http_any", std::vec!["*".to_string()]),
],
};
}
#[test]
fn new_rejects_disabled_endpoint() {
let result = crate::HttpClient::new(endpoint(false));
assert!(result.is_err());
}
#[test]
fn can_handle_matches_exact_role_and_kind() {
let client = match crate::HttpClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
assert!(client.can_handle("http_queries", "get_version"));
assert!(!client.can_handle("http_queries", "get_block"));
}
#[test]
fn can_handle_matches_wildcard_kind() {
let client = match crate::HttpClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
assert!(client.can_handle("http_any", "send_transaction"));
}
#[test]
fn snapshot_preserves_endpoint_metadata() {
let client = match crate::HttpClient::new(endpoint(true)) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
};
let snapshot = client.snapshot();
assert_eq!(snapshot.endpoint_name, "http_a");
assert_eq!(snapshot.provider, "test");
assert_eq!(snapshot.endpoint_url, "https://example.invalid");
assert_eq!(snapshot.roles.len(), 3);
}
#[test]
fn classify_method_detects_transaction_submission() {
for method in ["requestAirdrop", "sendTransaction"] {
assert_eq!(
crate::HttpClient::classify_method(method),
crate::HttpMethodClass::SendTransaction
);
}
}
#[test]
fn classify_method_detects_heavy_reads() {
assert_eq!(
crate::HttpClient::classify_method("getBlock"),
crate::HttpMethodClass::HeavyRead
);
assert_eq!(
crate::HttpClient::classify_method("getProgramAccounts"),
crate::HttpMethodClass::HeavyRead
);
assert_eq!(
crate::HttpClient::classify_method("getSignaturesForAddress"),
crate::HttpMethodClass::HeavyRead
);
assert_eq!(
crate::HttpClient::classify_method("simulateTransaction"),
crate::HttpMethodClass::HeavyRead
);
}
#[test]
fn every_standard_method_uses_its_declared_routing_class() {
for method in &crate::STANDARD_HTTP_METHODS {
assert_eq!(crate::HttpClient::classify_method(method.method), method.method_class());
}
}
#[test]
fn classify_method_defaults_to_general_rpc() {
assert_eq!(
crate::HttpClient::classify_method("getVersion"),
crate::HttpMethodClass::GeneralRpc
);
}
}

View File

@@ -0,0 +1,279 @@
// file: kb-onchain-transport/src/http_pool.rs
// version: 7
//! HTTP endpoint pool and role-based routing.
/// Pool of HTTP JSON-RPC endpoints.
#[derive(Clone, Debug)]
pub struct HttpEndpointPool {
clients: std::vec::Vec<crate::HttpClient>,
next_index: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl crate::HttpEndpointPool {
/// Builds a pool from the active profile HTTP endpoint list.
pub fn from_profile(profile: &kb_config::ProfileConfig) -> kb_core::Result<Self> {
let mut clients = std::vec::Vec::new();
for endpoint in &profile.solana.http_endpoints {
if !endpoint.enabled {
continue;
}
let client = match crate::HttpClient::new(endpoint.clone()) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
clients.push(client);
}
return crate::HttpEndpointPool::new(clients);
}
/// Creates a pool from already constructed clients.
pub fn new(clients: std::vec::Vec<crate::HttpClient>) -> kb_core::Result<Self> {
if clients.is_empty() {
tracing::error!(target: crate::TRACING_TARGET, action = "create_http_pool", error_code = "http_pool_empty", "HTTP endpoint pool has no enabled endpoint");
return std::result::Result::Err(kb_core::Error::config(
"http endpoint pool requires at least one enabled endpoint".to_string(),
));
}
tracing::debug!(target: crate::TRACING_TARGET, action = "create_http_pool", endpoint_count = clients.len(), "HTTP endpoint pool created");
return std::result::Result::Ok(Self {
clients,
next_index: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
});
}
/// Returns a serializable snapshot of every endpoint in the pool.
pub fn snapshot(&self) -> std::vec::Vec<crate::HttpPoolClientSnapshot> {
let mut snapshots = std::vec::Vec::new();
for client in &self.clients {
snapshots.push(client.snapshot());
}
return snapshots;
}
/// Selects one endpoint for the requested role and method.
pub fn select_client_for_role_and_method(
&self,
required_role: &str,
method: &str,
) -> kb_core::Result<crate::HttpClient> {
let request_kind = crate::request_kind_from_method(method);
return self.select_client_for_role_and_kind(required_role, &request_kind);
}
/// Selects one endpoint for the requested role and request kind.
pub fn select_client_for_role_and_kind(
&self,
required_role: &str,
request_kind: &str,
) -> kb_core::Result<crate::HttpClient> {
if self.clients.is_empty() {
return std::result::Result::Err(kb_core::Error::not_connected(
"http endpoint pool has no clients".to_string(),
));
}
let start_index = self.next_index.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let client_count = self.clients.len();
let mut offset = 0_usize;
while offset < client_count {
let index = (start_index + offset) % client_count;
let client = self.clients[index].clone();
if client.can_handle(required_role, request_kind) {
tracing::debug!(target: crate::TRACING_TARGET, action = "select_http_endpoint", required_role, request_kind, endpoint_name = %client.endpoint_name(), provider = %client.provider(), "selected HTTP endpoint");
return std::result::Result::Ok(client);
}
offset += 1;
}
tracing::error!(target: crate::TRACING_TARGET, action = "select_http_endpoint", required_role, request_kind, endpoint_count = self.clients.len(), error_code = "http_endpoint_not_found", "no HTTP endpoint supports requested role and kind");
return std::result::Result::Err(kb_core::Error::config(format!(
"no http endpoint supports role '{}' and request kind '{}'",
required_role, request_kind
)));
}
/// Executes one typed standard HTTP request through an endpoint selected by role.
pub async fn execute_standard_request_for_role<Request>(
&self,
required_role: &str,
request: &Request,
) -> kb_core::Result<<Request as crate::StandardHttpRequest>::Response>
where
Request: crate::StandardHttpRequest,
{
let client = match self.select_client_for_role_and_method(
required_role,
<Request as crate::StandardHttpRequest>::METHOD,
) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_standard_request(request).await;
}
/// Executes one explicitly registered standard HTTP method through the selected endpoint.
pub async fn execute_standard_method_raw_for_role(
&self,
required_role: &str,
method: &crate::StandardHttpMethodSpec,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
let client = match self.select_client_for_role_and_method(required_role, method.method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_standard_method_raw(method, params).await;
}
/// Executes one JSON-RPC request through the selected endpoint.
pub async fn execute_json_rpc_result_raw_for_role(
&self,
required_role: &str,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> kb_core::Result<serde_json::Value> {
let client = match self.select_client_for_role_and_method(required_role, &method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.execute_json_rpc_result_raw(method, params).await;
}
}
#[cfg(test)]
mod tests {
fn role_config(
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::EndpointRoleConfig {
return kb_config::EndpointRoleConfig {
role: role.to_string(),
enabled: true,
request_kinds,
priority: 1,
requests_per_second: 10,
burst_capacity: 10,
max_concurrent_requests: 4,
max_subscriptions: 16,
pause_after_rate_limit_ms: 1500,
};
}
fn endpoint(
name: &str,
role: &str,
request_kinds: std::vec::Vec<std::string::String>,
) -> kb_config::HttpEndpointConfig {
return kb_config::HttpEndpointConfig {
name: name.to_string(),
enabled: true,
provider: "test".to_string(),
cluster: "devnet".to_string(),
url: format!("https://{name}.invalid"),
connect_timeout_ms: 100,
request_timeout_ms: 100,
max_idle_connections_per_host: 2,
roles: std::vec![role_config(role, request_kinds)],
};
}
fn client(endpoint: kb_config::HttpEndpointConfig) -> crate::HttpClient {
match crate::HttpClient::new(endpoint) {
std::result::Result::Ok(client) => return client,
std::result::Result::Err(error) => panic!("client creation failed: {error}"),
}
}
#[test]
fn new_rejects_empty_pool() {
let result = crate::HttpEndpointPool::new(std::vec::Vec::new());
assert!(result.is_err());
}
#[test]
fn snapshot_lists_every_client() {
let pool = match crate::HttpEndpointPool::new(std::vec![
client(endpoint("a", "http_queries", std::vec!["get_version".to_string()])),
client(endpoint("b", "http_queries", std::vec!["get_version".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let snapshot = pool.snapshot();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot[0].endpoint_name, "a");
assert_eq!(snapshot[1].endpoint_name, "b");
}
#[test]
fn select_client_round_robins_matching_clients() {
let pool = match crate::HttpEndpointPool::new(std::vec![
client(endpoint("a", "http_queries", std::vec!["get_version".to_string()])),
client(endpoint("b", "http_queries", std::vec!["get_version".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let first = match pool.select_client_for_role_and_method("http_queries", "getVersion") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
let second = match pool.select_client_for_role_and_method("http_queries", "getVersion") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(first.endpoint_name(), "a");
assert_eq!(second.endpoint_name(), "b");
}
#[test]
fn select_client_skips_unsupported_clients() {
let pool = match crate::HttpEndpointPool::new(std::vec![
client(endpoint("a", "http_heavy", std::vec!["get_block".to_string()])),
client(endpoint("b", "http_queries", std::vec!["get_version".to_string()])),
]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let selected = match pool.select_client_for_role_and_method("http_queries", "getVersion") {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(selected.endpoint_name(), "b");
}
#[test]
fn standard_method_selection_uses_the_canonical_request_kind() {
let pool = match crate::HttpEndpointPool::new(std::vec![client(endpoint(
"a",
"http_queries",
std::vec!["get_version".to_string()],
))]) {
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let method = match crate::standard_http_method("getVersion") {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("getVersion specification missing"),
};
let selected = match pool.select_client_for_role_and_method("http_queries", method.method) {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => panic!("selection failed: {error}"),
};
assert_eq!(selected.endpoint_name(), "a");
}
#[test]
fn select_client_returns_error_for_missing_role() {
let pool = match crate::HttpEndpointPool::new(std::vec![client(endpoint(
"a",
"http_queries",
std::vec!["get_version".to_string()]
)),])
{
std::result::Result::Ok(pool) => pool,
std::result::Result::Err(error) => panic!("pool creation failed: {error}"),
};
let selected = pool.select_client_for_role_and_kind("http_heavy", "get_block");
assert!(selected.is_err());
}
}

View File

@@ -0,0 +1,315 @@
// file: kb-onchain-transport/src/json_rpc.rs
// version: 4
//! JSON-RPC 2.0 envelopes used by Solana HTTP and WebSocket transports.
/// Generic JSON-RPC 2.0 request.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcRequest {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Client request identifier.
pub id: serde_json::Value,
/// RPC method name.
pub method: std::string::String,
/// Ordered method parameters.
pub params: std::vec::Vec<serde_json::Value>,
}
impl crate::JsonRpcRequest {
/// Creates a request with a numeric identifier.
pub fn new_with_u64_id(
id: u64,
method: std::string::String,
params: std::vec::Vec<serde_json::Value>,
) -> Self {
return Self {
jsonrpc: "2.0".to_string(),
id: serde_json::Value::from(id),
method,
params,
};
}
/// Serializes the request into a compact JSON string.
pub fn to_json_string(&self) -> kb_core::Result<std::string::String> {
let serialization_result = serde_json::to_string(self);
return match serialization_result {
std::result::Result::Ok(text) => std::result::Result::Ok(text),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot serialize json-rpc request '{}': {error}", self.method),
)),
};
}
}
/// JSON-RPC 2.0 success response.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcSuccessResponse {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Result payload.
pub result: serde_json::Value,
/// Request identifier echoed by the server.
pub id: serde_json::Value,
}
/// JSON-RPC 2.0 error object.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcErrorObject {
/// Numeric JSON-RPC error code.
pub code: i64,
/// Human-readable error message.
pub message: std::string::String,
/// Optional server-provided payload.
pub data: std::option::Option<serde_json::Value>,
}
/// JSON-RPC 2.0 error response.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcErrorResponse {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Error payload.
pub error: crate::JsonRpcErrorObject,
/// Request identifier echoed by the server.
pub id: serde_json::Value,
}
/// JSON-RPC 2.0 notification parameters.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcNotificationParams {
/// Method-specific result payload.
pub result: serde_json::Value,
/// Remote subscription identifier.
pub subscription: u64,
}
/// JSON-RPC 2.0 notification message.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct JsonRpcNotification {
/// JSON-RPC version, expected to be `"2.0"`.
pub jsonrpc: std::string::String,
/// Notification method name.
pub method: std::string::String,
/// Notification payload.
pub params: crate::JsonRpcNotificationParams,
}
/// Parsed JSON-RPC response or notification.
#[derive(Clone, Debug, PartialEq)]
pub enum JsonRpcResponse {
/// Success response.
Success(crate::JsonRpcSuccessResponse),
/// Error response.
Error(crate::JsonRpcErrorResponse),
/// Notification message.
Notification(crate::JsonRpcNotification),
}
impl crate::JsonRpcResponse {
/// Returns a stable diagnostic kind name.
pub fn kind_name(&self) -> &'static str {
return match self {
Self::Success(_) => "success",
Self::Error(_) => "error",
Self::Notification(_) => "notification",
};
}
/// Converts the parsed response into a JSON value for UI display.
pub fn to_value(&self) -> kb_core::Result<serde_json::Value> {
return match self {
Self::Success(response) => {
let value_result = serde_json::to_value(response);
match value_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(error.to_string()))
},
}
},
Self::Error(response) => {
let value_result = serde_json::to_value(response);
match value_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(error.to_string()))
},
}
},
Self::Notification(notification) => {
let value_result = serde_json::to_value(notification);
match value_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => {
std::result::Result::Err(kb_core::Error::json(error.to_string()))
},
}
},
};
}
}
/// Parses an incoming JSON-RPC text payload.
pub fn parse_json_rpc_text(text: &str) -> kb_core::Result<crate::JsonRpcResponse> {
let value = match serde_json::from_str::<serde_json::Value>(text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"cannot parse json-rpc text: {error}"
)));
},
};
let object = match value.as_object() {
std::option::Option::Some(object) => object,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::json(
"json-rpc payload must be an object".to_string(),
));
},
};
let has_method = object.contains_key("method");
let has_params = object.contains_key("params");
let has_result = object.contains_key("result");
let has_error = object.contains_key("error");
let has_id = object.contains_key("id");
if has_method && has_params && !has_id {
let parse_result = serde_json::from_value::<crate::JsonRpcNotification>(value);
return match parse_result {
std::result::Result::Ok(notification) => {
std::result::Result::Ok(crate::JsonRpcResponse::Notification(notification))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot parse json-rpc notification: {error}"),
)),
};
}
if has_id && has_result && !has_error {
let parse_result = serde_json::from_value::<crate::JsonRpcSuccessResponse>(value);
return match parse_result {
std::result::Result::Ok(response) => {
std::result::Result::Ok(crate::JsonRpcResponse::Success(response))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot parse json-rpc success response: {error}"),
)),
};
}
if has_id && has_error && !has_result {
let parse_result = serde_json::from_value::<crate::JsonRpcErrorResponse>(value);
return match parse_result {
std::result::Result::Ok(response) => {
std::result::Result::Ok(crate::JsonRpcResponse::Error(response))
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(
format!("cannot parse json-rpc error response: {error}"),
)),
};
}
return std::result::Result::Err(kb_core::Error::json(
"unsupported json-rpc response shape".to_string(),
));
}
#[cfg(test)]
mod tests {
#[test]
fn request_serialization_contains_jsonrpc_version() {
let request = crate::JsonRpcRequest::new_with_u64_id(
1,
"getVersion".to_string(),
std::vec::Vec::new(),
);
let text = match request.to_json_string() {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert!(text.contains("\"jsonrpc\":\"2.0\""));
assert!(text.contains("\"method\":\"getVersion\""));
}
#[test]
fn request_serialization_preserves_params() {
let request = crate::JsonRpcRequest::new_with_u64_id(
9,
"getBalance".to_string(),
std::vec![serde_json::Value::String("account".to_string())],
);
let text = match request.to_json_string() {
std::result::Result::Ok(text) => text,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert!(text.contains("\"params\":[\"account\"]"));
}
#[test]
fn parses_success_response() {
let parsed = match crate::parse_json_rpc_text("{\"jsonrpc\":\"2.0\",\"result\":7,\"id\":1}")
{
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("response parsing failed: {error}"),
};
assert_eq!(parsed.kind_name(), "success");
}
#[test]
fn parses_error_response() {
let parsed = match crate::parse_json_rpc_text(
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32602,\"message\":\"invalid params\"},\"id\":2}",
) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("error response parsing failed: {error}"),
};
match parsed {
crate::JsonRpcResponse::Error(response) => {
assert_eq!(response.error.code, -32602);
assert_eq!(response.error.message, "invalid params");
},
_ => panic!("expected error response"),
}
}
#[test]
fn parses_notification_response() {
let text = "{\"jsonrpc\":\"2.0\",\"method\":\"slotNotification\",\"params\":{\"result\":123,\"subscription\":77}}";
let parsed = match crate::parse_json_rpc_text(text) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("notification parsing failed: {error}"),
};
match parsed {
crate::JsonRpcResponse::Notification(notification) => {
assert_eq!(notification.method, "slotNotification");
assert_eq!(notification.params.subscription, 77);
},
_ => panic!("expected notification"),
}
}
#[test]
fn parses_rejects_non_object_payload() {
let parsed = crate::parse_json_rpc_text("[1,2,3]");
assert!(parsed.is_err());
}
#[test]
fn parses_rejects_unsupported_shape() {
let parsed = crate::parse_json_rpc_text("{\"jsonrpc\":\"2.0\",\"method\":\"x\",\"id\":1}");
assert!(parsed.is_err());
}
#[test]
fn response_to_value_preserves_success_result() {
let parsed = match crate::parse_json_rpc_text(
"{\"jsonrpc\":\"2.0\",\"result\":{\"solana-core\":\"x\"},\"id\":1}",
) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => panic!("response parsing failed: {error}"),
};
let value = match parsed.to_value() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("response value conversion failed: {error}"),
};
assert_eq!(value["result"]["solana-core"].as_str(), std::option::Option::Some("x"));
}
}

View File

@@ -1,13 +1,389 @@
// file: kb-onchain-transport/src/lib.rs
// version: 2
// version: 4
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
//! Solana on-chain transport contracts.
//! Solana on-chain HTTP JSON-RPC transport and standard method contracts.
/// Returns the crate migration status.
pub fn migration_status() -> &'static str {
return "scaffolded";
}
mod client;
mod constants;
mod endpoint_role;
mod execution_rpc;
mod http_client;
mod http_pool;
mod json_rpc;
mod standard_http;
mod standard_http_accounts;
mod standard_http_blocks;
mod standard_http_cluster;
mod standard_http_economics;
mod standard_http_tokens;
mod standard_http_transactions;
mod standard_methods;
mod validation;
/// RPC endpoint configuration.
pub use self::client::RpcEndpoint;
/// Minimal Solana RPC client abstraction.
pub use self::client::SolanaRpcClient;
/// Canonical tracing target for this crate.
pub(crate) use self::constants::TRACING_TARGET;
/// Endpoint role snapshot shared by HTTP and WebSocket pools.
pub use self::endpoint_role::EndpointRoleSnapshot;
/// Converts a JSON-RPC method name into a stable request kind.
pub use self::endpoint_role::request_kind_from_method;
/// Contextual account information result.
pub use self::execution_rpc::AccountInfoResult;
/// Bounded account metadata and optional complete decoded data returned by `getAccountInfo`.
pub use self::execution_rpc::AccountInfoValue;
/// Signature returned by a faucet airdrop request.
pub use self::execution_rpc::AirdropResult;
/// Lamport balance returned for one account.
pub use self::execution_rpc::BalanceResult;
/// Current block height returned by the node.
pub use self::execution_rpc::BlockHeightResult;
/// Bounded transaction confirmation policy.
pub use self::execution_rpc::ConfirmTransactionConfig;
/// Current epoch and slot progression returned by `getEpochInfo`.
pub use self::execution_rpc::EpochInfoResult;
/// Fee estimate returned for one serialized message.
pub use self::execution_rpc::FeeForMessageResult;
/// Genesis hash and known public-cluster classification.
pub use self::execution_rpc::GenesisHashResult;
/// Configuration for `getAccountInfo`.
pub use self::execution_rpc::GetAccountInfoConfig;
/// Configuration for `getBalance`.
pub use self::execution_rpc::GetBalanceConfig;
/// Configuration for `getBlockHeight`.
pub use self::execution_rpc::GetBlockHeightConfig;
/// Configuration for `getEpochInfo`.
pub use self::execution_rpc::GetEpochInfoConfig;
/// Configuration for `getFeeForMessage`.
pub use self::execution_rpc::GetFeeForMessageConfig;
/// Configuration for `getLatestBlockhash`.
pub use self::execution_rpc::GetLatestBlockhashConfig;
/// Configuration for `getMinimumBalanceForRentExemption`.
pub use self::execution_rpc::GetMinimumBalanceForRentExemptionConfig;
/// Configuration for `getSignatureStatuses`.
pub use self::execution_rpc::GetSignatureStatusesConfig;
/// Latest recent blockhash returned by the cluster.
pub use self::execution_rpc::LatestBlockhashResult;
/// Rent-exempt minimum for one account data length.
pub use self::execution_rpc::MinimumBalanceForRentExemptionResult;
/// Configuration for `requestAirdrop`.
pub use self::execution_rpc::RequestAirdropConfig;
/// Commitment level accepted by execution-oriented RPC methods.
pub use self::execution_rpc::RpcCommitmentLevel;
/// Standard context attached to Solana RPC responses.
pub use self::execution_rpc::RpcResponseContext;
/// Configuration for `sendTransaction`.
pub use self::execution_rpc::SendTransactionConfig;
/// Result returned after a node accepts a signed transaction.
pub use self::execution_rpc::SendTransactionResult;
/// Current status for one submitted signature.
pub use self::execution_rpc::SignatureStatus;
/// Positional status response for submitted signatures.
pub use self::execution_rpc::SignatureStatusesResult;
/// Configuration for `simulateTransaction`.
pub use self::execution_rpc::SimulateTransactionConfig;
/// Typed result returned by `simulateTransaction`.
pub use self::execution_rpc::SimulateTransactionResult;
/// Optional accounts requested from `simulateTransaction`.
pub use self::execution_rpc::SimulationAccountsConfig;
/// Replacement blockhash returned by simulation.
pub use self::execution_rpc::SimulationReplacementBlockhash;
/// Adapts a raw `getAccountInfo` result.
pub use self::execution_rpc::adapt_get_account_info_result;
/// Adapts a raw `getAccountInfo` result with an optional decoded-data limit.
pub(crate) use self::execution_rpc::adapt_get_account_info_result_with_data_limit;
/// Adapts a raw `getBalance` result.
pub use self::execution_rpc::adapt_get_balance_result;
/// Adapts a raw `getBlockHeight` result.
pub use self::execution_rpc::adapt_get_block_height_result;
/// Adapts a raw `getEpochInfo` result.
pub use self::execution_rpc::adapt_get_epoch_info_result;
/// Adapts a raw `getFeeForMessage` result.
pub use self::execution_rpc::adapt_get_fee_for_message_result;
/// Adapts a raw `getGenesisHash` result.
pub use self::execution_rpc::adapt_get_genesis_hash_result;
/// Adapts a raw `getLatestBlockhash` result.
pub use self::execution_rpc::adapt_get_latest_blockhash_result;
/// Adapts a raw `getMinimumBalanceForRentExemption` result.
pub use self::execution_rpc::adapt_get_minimum_balance_for_rent_exemption_result;
/// Adapts a raw `getSignatureStatuses` result.
pub use self::execution_rpc::adapt_get_signature_statuses_result;
/// Adapts a raw `requestAirdrop` result.
pub use self::execution_rpc::adapt_request_airdrop_result;
/// Adapts a raw `sendTransaction` result.
pub use self::execution_rpc::adapt_send_transaction_result;
/// Adapts a raw `simulateTransaction` result.
pub use self::execution_rpc::adapt_simulate_transaction_result;
/// Classifies a genesis hash as an official public cluster.
pub use self::execution_rpc::classify_genesis_hash;
/// HTTP JSON-RPC client bound to one endpoint.
pub use self::http_client::HttpClient;
/// HTTP method class used for routing diagnostics.
pub use self::http_client::HttpMethodClass;
/// Snapshot of one HTTP pool endpoint.
pub use self::http_client::HttpPoolClientSnapshot;
/// HTTP endpoint pool with role-based routing.
pub use self::http_pool::HttpEndpointPool;
/// JSON-RPC 2.0 error object.
pub use self::json_rpc::JsonRpcErrorObject;
/// JSON-RPC 2.0 error response.
pub use self::json_rpc::JsonRpcErrorResponse;
/// JSON-RPC 2.0 notification.
pub use self::json_rpc::JsonRpcNotification;
/// JSON-RPC 2.0 notification parameters.
pub use self::json_rpc::JsonRpcNotificationParams;
/// JSON-RPC 2.0 request.
pub use self::json_rpc::JsonRpcRequest;
/// JSON-RPC 2.0 response parsed from HTTP or WebSocket text.
pub use self::json_rpc::JsonRpcResponse;
/// JSON-RPC 2.0 success response.
pub use self::json_rpc::JsonRpcSuccessResponse;
/// Parses an incoming JSON-RPC text payload.
pub use self::json_rpc::parse_json_rpc_text;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcAccountBalance;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcAccountEncoding;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcAccountInfoConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcCommitmentConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcContextConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcDataSlice;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcKeyedAccount;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcMemcmp;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcMemcmpEncodedBytes;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcOptionalContext;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcProgramAccountFilter;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcProgramAccountsConfig;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcResponse;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcTokenAccountBalance;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcTokenAccountsFilter;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcTokenAmount;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcTransactionDetails;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcTransactionEncoding;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::RpcUiAccount;
/// Configurable standard HTTP request trait and shared wire contracts.
pub use self::standard_http::StandardHttpRequest;
/// Configurable account-oriented standard HTTP requests.
pub use self::standard_http_accounts::GetLargestAccountsRequest;
/// Configurable account-oriented standard HTTP requests.
pub use self::standard_http_accounts::GetMultipleAccountsRequest;
/// Configurable account-oriented standard HTTP requests.
pub use self::standard_http_accounts::GetProgramAccountsRequest;
/// Configurable account-oriented standard HTTP requests.
pub use self::standard_http_accounts::RpcLargestAccountsConfig;
/// Configurable account-oriented standard HTTP requests.
pub use self::standard_http_accounts::RpcLargestAccountsFilter;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetBlockCommitmentRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetBlockProductionRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetBlockRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetBlockTimeRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetBlocksRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetBlocksWithLimitRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetFirstAvailableBlockRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::GetRecentPerformanceSamplesRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::MinimumLedgerSlotRequest;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcBlockCommitment;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcBlockConfig;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcBlockProduction;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcBlockProductionConfig;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcBlockProductionConfigRange;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcBlockProductionRange;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcConfirmedBlock;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcPerformanceSample;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcReward;
/// Configurable block and ledger standard HTTP requests.
pub use self::standard_http_blocks::RpcRewardType;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetClusterNodesRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetEpochScheduleRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetHealthRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetHighestSnapshotSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetIdentityRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetLeaderScheduleRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetMaxRetransmitSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetMaxShredInsertSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetSlotLeaderRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetSlotLeadersRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetSlotRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetVersionRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::GetVoteAccountsRequest;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcContactInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcEpochSchedule;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcGetVoteAccountsConfig;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcIdentity;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcLeaderSchedule;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcLeaderScheduleConfig;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcSnapshotSlotInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcVersionInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcVoteAccountInfo;
/// Configurable cluster-oriented standard HTTP requests.
pub use self::standard_http_cluster::RpcVoteAccountStatus;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::GetInflationGovernorRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::GetInflationRateRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::GetInflationRewardRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::GetStakeMinimumDelegationRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::GetSupplyRequest;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcEpochConfig;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcInflationGovernor;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcInflationGovernorConfig;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcInflationRate;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcInflationReward;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcSupply;
/// Configurable economics-oriented standard HTTP requests.
pub use self::standard_http_economics::RpcSupplyConfig;
/// Configurable token-oriented standard HTTP requests.
pub use self::standard_http_tokens::GetTokenAccountBalanceRequest;
/// Configurable token-oriented standard HTTP requests.
pub use self::standard_http_tokens::GetTokenAccountsByDelegateRequest;
/// Configurable token-oriented standard HTTP requests.
pub use self::standard_http_tokens::GetTokenAccountsByOwnerRequest;
/// Configurable token-oriented standard HTTP requests.
pub use self::standard_http_tokens::GetTokenLargestAccountsRequest;
/// Configurable token-oriented standard HTTP requests.
pub use self::standard_http_tokens::GetTokenSupplyRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use self::standard_http_transactions::GetRecentPrioritizationFeesRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use self::standard_http_transactions::GetTransactionCountRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use self::standard_http_transactions::IsBlockhashValidRequest;
/// Configurable transaction-oriented standard HTTP requests.
pub use self::standard_http_transactions::RpcPrioritizationFee;
/// Every standard Solana HTTP JSON-RPC method.
pub use self::standard_methods::STANDARD_HTTP_METHODS;
/// Every standard Solana WebSocket subscription pair.
pub use self::standard_methods::STANDARD_WS_SUBSCRIPTIONS;
/// Official category used to group standard Solana HTTP methods.
pub use self::standard_methods::StandardHttpCategory;
/// One canonical standard Solana HTTP method specification.
pub use self::standard_methods::StandardHttpMethodSpec;
/// Documentation-level contract exposed for a standard RPC method.
pub use self::standard_methods::StandardRpcContract;
/// Stability of one standard Solana WebSocket subscription surface.
pub use self::standard_methods::StandardWsStability;
/// One canonical standard Solana WebSocket subscribe/unsubscribe pair.
pub use self::standard_methods::StandardWsSubscriptionSpec;
/// Finds one exact standard HTTP method specification.
pub use self::standard_methods::standard_http_method;
/// Finds one standard WebSocket subscription from its subscribe or unsubscribe method.
pub use self::standard_methods::standard_ws_subscription;
/// Validates one base58 Solana blockhash or genesis hash.
pub use self::validation::validate_solana_hash_text;
/// Validates one base58 Solana public key.
pub use self::validation::validate_solana_pubkey_text;
/// Validates one base58 Solana transaction signature.
pub use self::validation::validate_transaction_signature_text;
/// Internal DEVNET_GENESIS_HASH contract.
pub(crate) use self::constants::DEVNET_GENESIS_HASH;
/// Internal MAINNET_GENESIS_HASH contract.
pub(crate) use self::constants::MAINNET_GENESIS_HASH;
/// Internal MAX_BLOCK_RANGE contract.
pub(crate) use self::constants::MAX_BLOCK_RANGE;
/// Internal MAX_CONFIRMATION_ATTEMPTS contract.
pub(crate) use self::constants::MAX_CONFIRMATION_ATTEMPTS;
/// Internal MAX_CONFIRMATION_POLL_INTERVAL_MS contract.
pub(crate) use self::constants::MAX_CONFIRMATION_POLL_INTERVAL_MS;
/// Internal MAX_EXECUTION_ACCOUNT_DATA_BYTES contract.
pub(crate) use self::constants::MAX_EXECUTION_ACCOUNT_DATA_BYTES;
/// Internal MAX_EXECUTION_RPC_BASE64_LENGTH contract.
pub(crate) use self::constants::MAX_EXECUTION_RPC_BASE64_LENGTH;
/// Internal MAX_MEMCMP_BASE58_LENGTH contract.
pub(crate) use self::constants::MAX_MEMCMP_BASE58_LENGTH;
/// Internal MAX_MEMCMP_BASE64_LENGTH contract.
pub(crate) use self::constants::MAX_MEMCMP_BASE64_LENGTH;
/// Internal MAX_MEMCMP_DECODED_BYTES contract.
pub(crate) use self::constants::MAX_MEMCMP_DECODED_BYTES;
/// Internal MAX_MULTIPLE_ACCOUNT_COUNT contract.
pub(crate) use self::constants::MAX_MULTIPLE_ACCOUNT_COUNT;
/// Internal MAX_PERFORMANCE_SAMPLE_COUNT contract.
pub(crate) use self::constants::MAX_PERFORMANCE_SAMPLE_COUNT;
/// Internal MAX_PRIORITIZATION_FEE_ACCOUNT_COUNT contract.
pub(crate) use self::constants::MAX_PRIORITIZATION_FEE_ACCOUNT_COUNT;
/// Internal MAX_SIGNATURE_STATUS_COUNT contract.
pub(crate) use self::constants::MAX_SIGNATURE_STATUS_COUNT;
/// Internal MAX_SIMULATION_ACCOUNT_COUNT contract.
pub(crate) use self::constants::MAX_SIMULATION_ACCOUNT_COUNT;
/// Internal MAX_SLOT_LEADER_COUNT contract.
pub(crate) use self::constants::MAX_SLOT_LEADER_COUNT;
/// Internal TESTNET_GENESIS_HASH contract.
pub(crate) use self::constants::TESTNET_GENESIS_HASH;
/// Internal role_matches contract.
pub(crate) use self::endpoint_role::role_matches;
/// Internal serialize_parameter contract.
pub(crate) use self::standard_http::serialize_parameter;
/// Internal validate_pubkey_list contract.
pub(crate) use self::standard_http::validate_pubkey_list;

View File

@@ -0,0 +1,499 @@
// file: kb-onchain-transport/src/standard_http.rs
// version: 3
//! Shared contracts for configurable standard Solana HTTP JSON-RPC requests.
use base64::Engine; // rust-rules: trait-import
/// Typed request contract for one standard Solana HTTP JSON-RPC method.
pub trait StandardHttpRequest {
/// Method-specific response decoded from the JSON-RPC `result` value.
type Response: serde::de::DeserializeOwned;
/// Exact standard Solana JSON-RPC method name.
const METHOD: &'static str;
/// Builds the exact positional JSON-RPC parameter array.
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>>;
}
/// Standard contextual Solana RPC response.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcResponse<T> {
/// Slot and optional API version used by the node.
pub context: crate::RpcResponseContext,
/// Method-specific response value.
pub value: T,
}
/// Response that may be returned directly or wrapped with an RPC context.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum RpcOptionalContext<T> {
/// Contextual response form.
Context(crate::RpcResponse<T>),
/// Backward-compatible response form without context.
Value(T),
}
/// Commitment and minimum-context options shared by standard read methods.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcContextConfig {
/// Optional commitment level. Absence delegates the default to the endpoint.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum slot at which the request may be evaluated.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
}
/// Commitment-only options used by methods that do not accept `minContextSlot`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcCommitmentConfig {
/// Optional commitment level. Absence delegates the default to the endpoint.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
/// Account-data encoding accepted by standard account RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum RpcAccountEncoding {
/// Legacy binary account data encoding alias.
#[serde(rename = "binary")]
Binary,
/// Legacy base58 account data encoding.
#[serde(rename = "base58")]
Base58,
/// Base64 account data encoding.
#[serde(rename = "base64")]
Base64,
/// Zstandard-compressed base64 account data encoding.
#[serde(rename = "base64+zstd")]
Base64Zstd,
/// Program-aware parsed JSON account data.
#[serde(rename = "jsonParsed")]
JsonParsed,
}
/// Transaction encoding accepted by block and transaction RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum RpcTransactionEncoding {
/// Legacy binary transaction encoding.
#[serde(rename = "binary")]
Binary,
/// Base58 transaction encoding.
#[serde(rename = "base58")]
Base58,
/// Base64 transaction encoding.
#[serde(rename = "base64")]
Base64,
/// Structured JSON transaction encoding.
#[serde(rename = "json")]
Json,
/// Program-aware parsed JSON transaction encoding.
#[serde(rename = "jsonParsed")]
JsonParsed,
}
/// Transaction detail level accepted by block-oriented RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcTransactionDetails {
/// Full transactions and metadata.
Full,
/// Signatures only.
Signatures,
/// No transaction entries.
None,
/// Account lists without full transaction data.
Accounts,
}
/// Optional byte range requested from account data.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcDataSlice {
/// Byte offset from the start of account data.
pub offset: usize,
/// Number of bytes requested.
pub length: usize,
}
impl crate::RpcDataSlice {
fn validate(&self) -> kb_core::Result<()> {
if self.offset.checked_add(self.length).is_none() {
return std::result::Result::Err(kb_core::Error::config(
"account data slice offset and length overflow usize",
));
}
return std::result::Result::Ok(());
}
}
/// Configurable account representation shared by account and token methods.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcAccountInfoConfig {
/// Optional account-data encoding.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub encoding: std::option::Option<crate::RpcAccountEncoding>,
/// Optional account-data byte slice.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub data_slice: std::option::Option<crate::RpcDataSlice>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum context slot.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
}
impl crate::RpcAccountInfoConfig {
/// Validates combinations that the standard account RPC contract cannot represent.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(data_slice) = self.data_slice {
let slice_result = data_slice.validate();
if let std::result::Result::Err(error) = slice_result {
return std::result::Result::Err(error);
}
if self.encoding == std::option::Option::Some(crate::RpcAccountEncoding::JsonParsed) {
return std::result::Result::Err(kb_core::Error::config(
"jsonParsed account encoding cannot be combined with dataSlice",
));
}
}
return std::result::Result::Ok(());
}
}
/// Encoded bytes used by a program-account memcmp filter.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase", tag = "encoding", content = "bytes")]
pub enum RpcMemcmpEncodedBytes {
/// Base58-encoded bytes.
Base58(std::string::String),
/// Base64-encoded bytes.
Base64(std::string::String),
/// Explicit raw byte array.
Bytes(std::vec::Vec<u8>),
}
impl crate::RpcMemcmpEncodedBytes {
fn validate(&self) -> kb_core::Result<()> {
let decoded_length = match self {
Self::Base58(value) => {
if value.len() > crate::MAX_MEMCMP_BASE58_LENGTH {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base58 value must not exceed {} characters",
crate::MAX_MEMCMP_BASE58_LENGTH
)));
}
let decoded = match bs58::decode(value).into_vec() {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base58 value is invalid: {error}"
)));
},
};
decoded.len()
},
Self::Base64(value) => {
if value.len() > crate::MAX_MEMCMP_BASE64_LENGTH {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base64 value must not exceed {} characters",
crate::MAX_MEMCMP_BASE64_LENGTH
)));
}
let decoded = match base64::prelude::BASE64_STANDARD.decode(value) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base64 value is invalid: {error}"
)));
},
};
decoded.len()
},
Self::Bytes(value) => value.len(),
};
if decoded_length > crate::MAX_MEMCMP_DECODED_BYTES {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp value must not exceed {} decoded bytes",
crate::MAX_MEMCMP_DECODED_BYTES
)));
}
return std::result::Result::Ok(());
}
}
/// Offset and encoded bytes used by a memcmp account filter.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcMemcmp {
/// Data offset at which the comparison starts.
pub offset: usize,
/// Bytes compared at the requested offset.
#[serde(flatten)]
pub bytes: crate::RpcMemcmpEncodedBytes,
}
/// One filter accepted by `getProgramAccounts` and `programSubscribe`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcProgramAccountFilter {
/// Exact account data size.
DataSize(u64),
/// Byte comparison at an account-data offset.
Memcmp(crate::RpcMemcmp),
/// Standard SPL Token account state filter.
TokenAccountState,
}
impl crate::RpcProgramAccountFilter {
fn validate(&self) -> kb_core::Result<()> {
if let Self::Memcmp(memcmp) = self {
return memcmp.bytes.validate();
}
return std::result::Result::Ok(());
}
}
/// Configurable `getProgramAccounts` request options.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcProgramAccountsConfig {
/// Optional account filters, evaluated by the node in the provided order.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub filters: std::option::Option<std::vec::Vec<crate::RpcProgramAccountFilter>>,
/// Optional account-data encoding.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub encoding: std::option::Option<crate::RpcAccountEncoding>,
/// Optional account-data byte slice.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub data_slice: std::option::Option<crate::RpcDataSlice>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum context slot.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
/// Whether the response must include a context wrapper.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub with_context: std::option::Option<bool>,
/// Optional validator-side deterministic result sorting.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub sort_results: std::option::Option<bool>,
}
impl crate::RpcProgramAccountsConfig {
/// Validates account encoding, data slicing and every filter.
pub fn validate(&self) -> kb_core::Result<()> {
let account_config = crate::RpcAccountInfoConfig {
encoding: self.encoding,
data_slice: self.data_slice,
commitment: self.commitment,
min_context_slot: self.min_context_slot,
};
let account_result = account_config.validate();
if let std::result::Result::Err(error) = account_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(filters) = &self.filters {
for filter in filters {
let filter_result = filter.validate();
if let std::result::Result::Err(error) = filter_result {
return std::result::Result::Err(error);
}
}
}
return std::result::Result::Ok(());
}
}
/// Mint or Token Program selector used by token-account queries.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcTokenAccountsFilter {
/// Select token accounts for one mint.
Mint(std::string::String),
/// Select accounts owned by one Token Program generation.
ProgramId(std::string::String),
}
impl crate::RpcTokenAccountsFilter {
/// Validates the public key embedded in the selected filter.
pub fn validate(&self) -> kb_core::Result<()> {
let value = match self {
Self::Mint(value) | Self::ProgramId(value) => value,
};
return crate::validate_solana_pubkey_text(value, "token account filter public key");
}
}
/// Account representation returned by configurable standard account methods.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcUiAccount {
/// Account lamports.
pub lamports: u64,
/// Owner Program ID.
pub owner: std::string::String,
/// Whether the account is executable.
pub executable: bool,
/// Rent epoch reported by the node.
pub rent_epoch: u64,
/// Account data length when exposed by the node.
#[serde(default)]
pub space: std::option::Option<u64>,
/// Encoding-dependent account data payload.
pub data: serde_json::Value,
}
/// Public key and account pair returned by program and token-account scans.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcKeyedAccount {
/// Account public key.
pub pubkey: std::string::String,
/// Account state and data.
pub account: crate::RpcUiAccount,
}
/// Token amount with exact integer and decimal string representations.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcTokenAmount {
/// Raw token amount as an unsigned decimal string.
pub amount: std::string::String,
/// Mint decimal precision.
pub decimals: u8,
/// Optional floating representation retained for wire compatibility.
pub ui_amount: std::option::Option<f64>,
/// Exact decimal display string.
pub ui_amount_string: std::string::String,
}
/// Token account address and balance returned by `getTokenLargestAccounts`.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcTokenAccountBalance {
/// Token account public key.
pub address: std::string::String,
/// Raw token amount as an unsigned decimal string.
pub amount: std::string::String,
/// Mint decimal precision.
pub decimals: u8,
/// Optional floating representation retained for wire compatibility.
pub ui_amount: std::option::Option<f64>,
/// Exact decimal display string.
pub ui_amount_string: std::string::String,
}
/// One lamport-ranked account returned by `getLargestAccounts`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcAccountBalance {
/// Account public key.
pub address: std::string::String,
/// Lamport balance.
pub lamports: u64,
}
pub(crate) fn serialize_parameter<T: serde::Serialize>(
method: &str,
value: &T,
) -> kb_core::Result<serde_json::Value> {
return match serde_json::to_value(value) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::json(format!(
"cannot serialize {method} parameter: {error}"
))),
};
}
pub(crate) fn validate_pubkey_list(
values: &[std::string::String],
field: &str,
maximum: usize,
) -> kb_core::Result<()> {
if values.len() > maximum {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field} must not exceed {maximum} entries"
)));
}
for value in values {
let validation_result = crate::validate_solana_pubkey_text(value, field);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn optional_account_options_serialize_only_selected_fields() {
let config = crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::Base64Zstd),
data_slice: std::option::Option::Some(crate::RpcDataSlice { offset: 32, length: 64 }),
commitment: std::option::Option::None,
min_context_slot: std::option::Option::Some(91),
};
let value = match serde_json::to_value(config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("config serialization failed: {error}"),
};
assert_eq!(
value,
serde_json::json!({
"encoding": "base64+zstd",
"dataSlice": { "offset": 32, "length": 64 },
"minContextSlot": 91
})
);
let binary = match serde_json::to_value(crate::RpcAccountEncoding::Binary) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("binary encoding serialization failed: {error}")
},
};
assert_eq!(binary, serde_json::Value::String("binary".to_string()));
}
#[test]
fn memcmp_filter_preserves_encoding_and_enforces_decoded_bound() {
let filter = crate::RpcProgramAccountFilter::Memcmp(crate::RpcMemcmp {
offset: 8,
bytes: crate::RpcMemcmpEncodedBytes::Bytes(std::vec![1_u8, 2_u8, 3_u8]),
});
let value = match serde_json::to_value(&filter) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("filter serialization failed: {error}"),
};
assert_eq!(
value,
serde_json::json!({
"memcmp": { "offset": 8, "encoding": "bytes", "bytes": [1, 2, 3] }
})
);
let oversized = crate::RpcProgramAccountFilter::Memcmp(crate::RpcMemcmp {
offset: 0,
bytes: crate::RpcMemcmpEncodedBytes::Bytes(std::vec![0_u8; 129]),
});
assert!(oversized.validate().is_err());
}
#[test]
fn json_parsed_account_data_rejects_data_slice() {
let config = crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::JsonParsed),
data_slice: std::option::Option::Some(crate::RpcDataSlice { offset: 0, length: 1 }),
commitment: std::option::Option::None,
min_context_slot: std::option::Option::None,
};
assert!(config.validate().is_err());
}
}

View File

@@ -0,0 +1,212 @@
// file: kb-onchain-transport/src/standard_http_accounts.rs
// version: 2
//! Configurable standard account-oriented Solana HTTP JSON-RPC requests.
/// Optional circulating-supply filter for `getLargestAccounts`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcLargestAccountsFilter {
/// Accounts included in circulating supply.
Circulating,
/// Accounts excluded from circulating supply.
NonCirculating,
}
/// Options accepted by `getLargestAccounts`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcLargestAccountsConfig {
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional circulating-supply filter.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub filter: std::option::Option<crate::RpcLargestAccountsFilter>,
/// Optional validator-side deterministic sorting flag.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub sort_results: std::option::Option<bool>,
}
/// Typed `getLargestAccounts` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetLargestAccountsRequest {
/// Optional request configuration. `None` emits no parameter.
pub config: std::option::Option<crate::RpcLargestAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetLargestAccountsRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcAccountBalance>>;
const METHOD: &'static str = "getLargestAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getMultipleAccounts` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetMultipleAccountsRequest {
/// Account public keys in positional response order.
pub addresses: std::vec::Vec<std::string::String>,
/// Optional account encoding, slicing and context options.
pub config: std::option::Option<crate::RpcAccountInfoConfig>,
}
impl crate::StandardHttpRequest for crate::GetMultipleAccountsRequest {
type Response = crate::RpcResponse<std::vec::Vec<std::option::Option<crate::RpcUiAccount>>>;
const METHOD: &'static str = "getMultipleAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result = crate::validate_pubkey_list(
&self.addresses,
"getMultipleAccounts address",
crate::MAX_MULTIPLE_ACCOUNT_COUNT,
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(config) = &self.config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
}
let addresses = match crate::serialize_parameter(Self::METHOD, &self.addresses) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut params = std::vec![addresses];
if let std::option::Option::Some(config) = &self.config {
let config_value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getProgramAccounts` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetProgramAccountsRequest {
/// Program ID whose owned accounts are requested.
pub program_id: std::string::String,
/// Optional filters, account representation and contextual response options.
pub config: std::option::Option<crate::RpcProgramAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetProgramAccountsRequest {
type Response = crate::RpcOptionalContext<std::vec::Vec<crate::RpcKeyedAccount>>;
const METHOD: &'static str = "getProgramAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_solana_pubkey_text(&self.program_id, "getProgramAccounts program id");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(config) = &self.config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
}
let mut params = std::vec![serde_json::Value::String(self.program_id.clone())];
if let std::option::Option::Some(config) = &self.config {
let config_value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn multiple_accounts_preserves_optional_encoding_slice_and_context() {
let request = crate::GetMultipleAccountsRequest {
addresses: std::vec![pubkey(1), pubkey(2)],
config: std::option::Option::Some(crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::Base64),
data_slice: std::option::Option::Some(crate::RpcDataSlice { offset: 4, length: 8 }),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed),
min_context_slot: std::option::Option::Some(99),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params.len(), 2);
assert_eq!(params[0].as_array().map(std::vec::Vec::len), std::option::Option::Some(2));
assert_eq!(params[1]["encoding"], serde_json::Value::String("base64".to_string()));
assert_eq!(params[1]["minContextSlot"], serde_json::Value::from(99_u64));
}
#[test]
fn multiple_accounts_enforces_official_request_bound() {
let request = crate::GetMultipleAccountsRequest {
addresses: (0_u8..101_u8).map(pubkey).collect(),
config: std::option::Option::None,
};
assert!(crate::StandardHttpRequest::params(&request).is_err());
let empty = crate::GetMultipleAccountsRequest {
addresses: std::vec::Vec::new(),
config: std::option::Option::None,
};
let empty_params = match crate::StandardHttpRequest::params(&empty) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("empty params failed: {error}"),
};
assert_eq!(empty_params, std::vec![serde_json::json!([])]);
}
#[test]
fn program_accounts_preserves_filters_and_context_switch() {
let request = crate::GetProgramAccountsRequest {
program_id: pubkey(7),
config: std::option::Option::Some(crate::RpcProgramAccountsConfig {
filters: std::option::Option::Some(std::vec![
crate::RpcProgramAccountFilter::DataSize(165),
crate::RpcProgramAccountFilter::TokenAccountState,
]),
encoding: std::option::Option::Some(crate::RpcAccountEncoding::JsonParsed),
data_slice: std::option::Option::None,
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
min_context_slot: std::option::Option::None,
with_context: std::option::Option::Some(true),
sort_results: std::option::Option::Some(true),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["withContext"], serde_json::Value::Bool(true));
assert_eq!(params[1]["sortResults"], serde_json::Value::Bool(true));
assert_eq!(
params[1]["filters"][1],
serde_json::Value::String("tokenAccountState".to_string())
);
}
}

View File

@@ -0,0 +1,548 @@
// file: kb-onchain-transport/src/standard_http_blocks.rs
// version: 2
//! Configurable standard block and ledger Solana HTTP JSON-RPC requests.
/// Options accepted by `getBlock`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockConfig {
/// Optional transaction encoding.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub encoding: std::option::Option<crate::RpcTransactionEncoding>,
/// Optional transaction detail level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub transaction_details: std::option::Option<crate::RpcTransactionDetails>,
/// Whether rewards must be included.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub rewards: std::option::Option<bool>,
/// Optional confirmed or finalized commitment.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Highest transaction version the caller can decode.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub max_supported_transaction_version: std::option::Option<u8>,
}
impl crate::RpcBlockConfig {
/// Rejects the processed commitment unsupported by block-history methods.
pub fn validate(&self) -> kb_core::Result<()> {
if self.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed) {
return std::result::Result::Err(kb_core::Error::config(
"getBlock does not support processed commitment",
));
}
return std::result::Result::Ok(());
}
}
/// Inclusive slot range accepted by `getBlockProduction`.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProductionConfigRange {
/// First slot included in the range.
pub first_slot: u64,
/// Optional final slot included in the range.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub last_slot: std::option::Option<u64>,
}
/// Options accepted by `getBlockProduction`.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProductionConfig {
/// Optional validator identity filter.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub identity: std::option::Option<std::string::String>,
/// Optional inclusive slot range.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub range: std::option::Option<crate::RpcBlockProductionConfigRange>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
impl crate::RpcBlockProductionConfig {
/// Validates identity and range ordering.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(identity) = &self.identity {
let identity_result =
crate::validate_solana_pubkey_text(identity, "getBlockProduction identity");
if let std::result::Result::Err(error) = identity_result {
return std::result::Result::Err(error);
}
}
if let std::option::Option::Some(range) = self.range {
if let std::option::Option::Some(last_slot) = range.last_slot {
if last_slot < range.first_slot {
return std::result::Result::Err(kb_core::Error::config(
"getBlockProduction last slot must not precede first slot",
));
}
}
}
return std::result::Result::Ok(());
}
}
/// Reward category attached to a block reward entry.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RpcRewardType {
/// Transaction fee reward.
Fee,
/// Rent reward.
Rent,
/// Staking reward.
Staking,
/// Vote reward.
Voting,
}
/// One reward entry returned with a block.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcReward {
/// Recipient public key.
pub pubkey: std::string::String,
/// Signed lamport balance change.
pub lamports: i64,
/// Recipient balance after the reward.
pub post_balance: u64,
/// Optional reward category.
pub reward_type: std::option::Option<crate::RpcRewardType>,
/// Optional validator commission percentage.
pub commission: std::option::Option<u8>,
/// Optional validator commission in basis points.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub commission_bps: std::option::Option<u16>,
}
/// Encoding-dependent confirmed block returned by `getBlock`.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcConfirmedBlock {
/// Block hash.
pub blockhash: std::string::String,
/// Previous block hash.
pub previous_blockhash: std::string::String,
/// Parent slot.
pub parent_slot: u64,
/// Encoding-dependent transaction entries when requested.
#[serde(default)]
pub transactions: std::option::Option<std::vec::Vec<serde_json::Value>>,
/// Signature list when signature-only details are requested.
#[serde(default)]
pub signatures: std::option::Option<std::vec::Vec<std::string::String>>,
/// Rewards when requested.
#[serde(default)]
pub rewards: std::option::Option<std::vec::Vec<crate::RpcReward>>,
/// Unix block time when available.
#[serde(default)]
pub block_time: std::option::Option<i64>,
/// Block height when available.
#[serde(default)]
pub block_height: std::option::Option<u64>,
/// Number of reward partitions when available.
#[serde(default)]
pub num_reward_partitions: std::option::Option<u64>,
}
/// Stake commitment information returned for one block.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockCommitment {
/// Commitment stake by lockout depth, or `None` when unavailable.
pub commitment: std::option::Option<std::vec::Vec<u64>>,
/// Total active stake used for the commitment calculation.
pub total_stake: u64,
}
/// Actual slot range represented by a block-production response.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProductionRange {
/// First represented slot.
pub first_slot: u64,
/// Last represented slot.
pub last_slot: u64,
}
/// Block-production counts grouped by validator identity.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcBlockProduction {
/// Validator identity to `(leader slots, blocks produced)` map.
pub by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
/// Actual represented slot range.
pub range: crate::RpcBlockProductionRange,
}
/// Recent cluster performance sample.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcPerformanceSample {
/// Slot at the end of the sample window.
pub slot: u64,
/// Total transactions processed during the sample.
pub num_transactions: u64,
/// Optional count excluding vote transactions.
#[serde(default)]
pub num_non_vote_transactions: std::option::Option<u64>,
/// Slots processed during the sample.
pub num_slots: u64,
/// Sample period in seconds.
pub sample_period_secs: u16,
}
/// Typed `getBlock` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlockRequest {
/// Block slot.
pub slot: u64,
/// Optional encoding, details, rewards, commitment and version options.
pub config: std::option::Option<crate::RpcBlockConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlockRequest {
type Response = std::option::Option<crate::RpcConfirmedBlock>;
const METHOD: &'static str = "getBlock";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let mut params = std::vec![serde_json::Value::from(self.slot)];
if let std::option::Option::Some(config) = &self.config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getBlockCommitment` request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GetBlockCommitmentRequest {
/// Block slot.
pub slot: u64,
}
impl crate::StandardHttpRequest for crate::GetBlockCommitmentRequest {
type Response = crate::RpcBlockCommitment;
const METHOD: &'static str = "getBlockCommitment";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec![serde_json::Value::from(self.slot)]);
}
}
/// Typed `getBlockProduction` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetBlockProductionRequest {
/// Optional identity, range and commitment options.
pub config: std::option::Option<crate::RpcBlockProductionConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlockProductionRequest {
type Response = crate::RpcResponse<crate::RpcBlockProduction>;
const METHOD: &'static str = "getBlockProduction";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let validation_result = config.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getBlocks` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlocksRequest {
/// First slot included in the scan.
pub start_slot: u64,
/// Optional final slot included in the scan.
pub end_slot: std::option::Option<u64>,
/// Optional confirmed or finalized context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlocksRequest {
type Response = std::vec::Vec<u64>;
const METHOD: &'static str = "getBlocks";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(end_slot) = self.end_slot {
if end_slot >= self.start_slot
&& end_slot.saturating_sub(self.start_slot) > crate::MAX_BLOCK_RANGE
{
return std::result::Result::Err(kb_core::Error::config(format!(
"getBlocks range must not exceed {} slots",
crate::MAX_BLOCK_RANGE
)));
}
}
if let std::option::Option::Some(config) = &self.config {
if config.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed)
{
return std::result::Result::Err(kb_core::Error::config(
"getBlocks does not support processed commitment",
));
}
}
let mut params = std::vec![serde_json::Value::from(self.start_slot)];
if let std::option::Option::Some(end_slot) = self.end_slot {
params.push(serde_json::Value::from(end_slot));
}
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getBlocksWithLimit` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlocksWithLimitRequest {
/// First slot considered by the scan.
pub start_slot: u64,
/// Maximum number of block slots returned.
pub limit: u64,
/// Optional confirmed or finalized context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetBlocksWithLimitRequest {
type Response = std::vec::Vec<u64>;
const METHOD: &'static str = "getBlocksWithLimit";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if self.limit > crate::MAX_BLOCK_RANGE {
return std::result::Result::Err(kb_core::Error::config(format!(
"getBlocksWithLimit limit must not exceed {}",
crate::MAX_BLOCK_RANGE
)));
}
if let std::option::Option::Some(config) = &self.config {
if config.commitment == std::option::Option::Some(crate::RpcCommitmentLevel::Processed)
{
return std::result::Result::Err(kb_core::Error::config(
"getBlocksWithLimit does not support processed commitment",
));
}
}
let mut params = std::vec![
serde_json::Value::from(self.start_slot),
serde_json::Value::from(self.limit),
];
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getBlockTime` request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GetBlockTimeRequest {
/// Block slot.
pub slot: u64,
}
impl crate::StandardHttpRequest for crate::GetBlockTimeRequest {
type Response = std::option::Option<i64>;
const METHOD: &'static str = "getBlockTime";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec![serde_json::Value::from(self.slot)]);
}
}
/// Typed `getFirstAvailableBlock` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetFirstAvailableBlockRequest;
impl crate::StandardHttpRequest for crate::GetFirstAvailableBlockRequest {
type Response = u64;
const METHOD: &'static str = "getFirstAvailableBlock";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getRecentPerformanceSamples` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetRecentPerformanceSamplesRequest {
/// Optional sample count. Absence delegates the default to the endpoint.
pub limit: std::option::Option<usize>,
}
impl crate::StandardHttpRequest for crate::GetRecentPerformanceSamplesRequest {
type Response = std::vec::Vec<crate::RpcPerformanceSample>;
const METHOD: &'static str = "getRecentPerformanceSamples";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(limit) = self.limit {
if limit > crate::MAX_PERFORMANCE_SAMPLE_COUNT {
return std::result::Result::Err(kb_core::Error::config(format!(
"getRecentPerformanceSamples limit must not exceed {}",
crate::MAX_PERFORMANCE_SAMPLE_COUNT
)));
}
return std::result::Result::Ok(std::vec![serde_json::Value::from(limit)]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `minimumLedgerSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct MinimumLedgerSlotRequest;
impl crate::StandardHttpRequest for crate::MinimumLedgerSlotRequest {
type Response = u64;
const METHOD: &'static str = "minimumLedgerSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
#[cfg(test)]
mod tests {
#[test]
fn block_options_remain_independently_selectable() {
let request = crate::GetBlockRequest {
slot: 55,
config: std::option::Option::Some(crate::RpcBlockConfig {
encoding: std::option::Option::Some(crate::RpcTransactionEncoding::JsonParsed),
transaction_details: std::option::Option::Some(
crate::RpcTransactionDetails::Accounts,
),
rewards: std::option::Option::Some(false),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed),
max_supported_transaction_version: std::option::Option::Some(0),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["encoding"], serde_json::Value::String("jsonParsed".to_string()));
assert_eq!(
params[1]["transactionDetails"],
serde_json::Value::String("accounts".to_string())
);
assert_eq!(params[1]["rewards"], serde_json::Value::Bool(false));
assert_eq!(params[1]["maxSupportedTransactionVersion"], serde_json::Value::from(0_u64));
let reward = match serde_json::from_value::<crate::RpcReward>(serde_json::json!({
"pubkey": "validator",
"lamports": 42,
"postBalance": 84,
"rewardType": "voting",
"commission": 5,
"commissionBps": 550
})) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("reward parsing failed: {error}"),
};
assert_eq!(reward.commission, std::option::Option::Some(5));
assert_eq!(reward.commission_bps, std::option::Option::Some(550));
}
#[test]
fn blocks_without_end_slot_places_config_in_second_position() {
let request = crate::GetBlocksRequest {
start_slot: 10,
end_slot: std::option::Option::None,
config: std::option::Option::Some(crate::RpcContextConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
min_context_slot: std::option::Option::Some(9),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params.len(), 2);
assert_eq!(params[1]["commitment"], serde_json::Value::String("finalized".to_string()));
}
#[test]
fn block_ranges_and_performance_samples_are_bounded() {
let range = crate::GetBlocksRequest {
start_slot: 0,
end_slot: std::option::Option::Some(500_001),
config: std::option::Option::None,
};
let samples =
crate::GetRecentPerformanceSamplesRequest { limit: std::option::Option::Some(721) };
assert!(crate::StandardHttpRequest::params(&range).is_err());
assert!(crate::StandardHttpRequest::params(&samples).is_err());
let reversed_range = crate::GetBlocksRequest {
start_slot: 10,
end_slot: std::option::Option::Some(9),
config: std::option::Option::None,
};
let zero_blocks = crate::GetBlocksWithLimitRequest {
start_slot: 10,
limit: 0,
config: std::option::Option::None,
};
let zero_samples =
crate::GetRecentPerformanceSamplesRequest { limit: std::option::Option::Some(0) };
let reversed_params = match crate::StandardHttpRequest::params(&reversed_range) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("reversed range failed: {error}"),
};
let zero_block_params = match crate::StandardHttpRequest::params(&zero_blocks) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("zero block limit failed: {error}"),
};
let zero_sample_params = match crate::StandardHttpRequest::params(&zero_samples) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("zero sample limit failed: {error}"),
};
assert_eq!(
reversed_params,
std::vec![serde_json::Value::from(10_u64), serde_json::Value::from(9_u64),]
);
assert_eq!(
zero_block_params,
std::vec![serde_json::Value::from(10_u64), serde_json::Value::from(0_u64),]
);
assert_eq!(zero_sample_params, std::vec![serde_json::Value::from(0_u64)]);
}
}

View File

@@ -0,0 +1,505 @@
// file: kb-onchain-transport/src/standard_http_cluster.rs
// version: 2
//! Configurable standard cluster-oriented Solana HTTP JSON-RPC requests.
/// Contact information returned for one cluster node.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcContactInfo {
/// Node identity public key.
pub pubkey: std::string::String,
/// Gossip socket address.
pub gossip: std::option::Option<std::string::String>,
/// TVU UDP socket address.
pub tvu: std::option::Option<std::string::String>,
/// TPU UDP socket address.
pub tpu: std::option::Option<std::string::String>,
/// TPU QUIC socket address.
pub tpu_quic: std::option::Option<std::string::String>,
/// TPU forwarding UDP socket address.
pub tpu_forwards: std::option::Option<std::string::String>,
/// TPU forwarding QUIC socket address.
pub tpu_forwards_quic: std::option::Option<std::string::String>,
/// TPU vote socket address.
pub tpu_vote: std::option::Option<std::string::String>,
/// Repair service socket address.
pub serve_repair: std::option::Option<std::string::String>,
/// JSON-RPC socket address.
pub rpc: std::option::Option<std::string::String>,
/// PubSub socket address.
pub pubsub: std::option::Option<std::string::String>,
/// Validator software version.
pub version: std::option::Option<std::string::String>,
/// Validator client identifier.
pub client_id: std::option::Option<std::string::String>,
/// Feature-set identifier prefix.
pub feature_set: std::option::Option<u32>,
/// Shred version.
pub shred_version: std::option::Option<u16>,
}
/// Epoch schedule derived from the cluster genesis configuration.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcEpochSchedule {
/// Slots in a normal epoch.
pub slots_per_epoch: u64,
/// Leader-schedule offset in slots.
pub leader_schedule_slot_offset: u64,
/// Whether warmup epochs are enabled.
pub warmup: bool,
/// First epoch using the normal slot count.
pub first_normal_epoch: u64,
/// First slot of the first normal epoch.
pub first_normal_slot: u64,
}
/// Highest complete and incremental snapshot slots available from a node.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcSnapshotSlotInfo {
/// Highest full snapshot slot.
pub full: u64,
/// Highest incremental snapshot slot when available.
pub incremental: std::option::Option<u64>,
}
/// Node identity response.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcIdentity {
/// Node identity public key.
pub identity: std::string::String,
}
/// Options accepted by `getLeaderSchedule`.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcLeaderScheduleConfig {
/// Optional validator identity whose schedule is requested.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub identity: std::option::Option<std::string::String>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
impl crate::RpcLeaderScheduleConfig {
/// Validates the optional identity public key.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(identity) = &self.identity {
return crate::validate_solana_pubkey_text(identity, "getLeaderSchedule identity");
}
return std::result::Result::Ok(());
}
}
/// Leader schedule keyed by validator identity.
pub type RpcLeaderSchedule = std::collections::BTreeMap<std::string::String, std::vec::Vec<usize>>;
/// Validator software version information.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RpcVersionInfo {
/// Validator software version.
pub solana_core: std::string::String,
/// Feature-set identifier prefix.
pub feature_set: std::option::Option<u32>,
}
/// Options accepted by `getVoteAccounts`.
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcGetVoteAccountsConfig {
/// Optional vote account public key filter.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub vote_pubkey: std::option::Option<std::string::String>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Whether unstaked delinquent validators must be retained.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub keep_unstaked_delinquents: std::option::Option<bool>,
/// Optional delinquency threshold in slots.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub delinquent_slot_distance: std::option::Option<u64>,
}
impl crate::RpcGetVoteAccountsConfig {
/// Validates the optional vote account public key.
pub fn validate(&self) -> kb_core::Result<()> {
if let std::option::Option::Some(vote_pubkey) = &self.vote_pubkey {
return crate::validate_solana_pubkey_text(vote_pubkey, "getVoteAccounts vote account");
}
return std::result::Result::Ok(());
}
}
/// Vote account information returned by `getVoteAccounts`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcVoteAccountInfo {
/// Vote account public key.
pub vote_pubkey: std::string::String,
/// Validator identity public key.
pub node_pubkey: std::string::String,
/// Activated stake in lamports.
pub activated_stake: u64,
/// Vote commission percentage.
pub commission: u8,
/// Vote inflation-reward commission in basis points when exposed by the node.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub inflation_rewards_commission_bps: std::option::Option<u16>,
/// Whether the vote account is staked in the current epoch.
pub epoch_vote_account: bool,
/// `(epoch, credits, previous credits)` history.
pub epoch_credits: std::vec::Vec<(u64, u64, u64)>,
/// Most recent voted slot.
pub last_vote: u64,
/// Current root slot.
pub root_slot: u64,
}
/// Current and delinquent validator vote accounts.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct RpcVoteAccountStatus {
/// Current validator vote accounts.
pub current: std::vec::Vec<crate::RpcVoteAccountInfo>,
/// Delinquent validator vote accounts.
pub delinquent: std::vec::Vec<crate::RpcVoteAccountInfo>,
}
/// Typed `getClusterNodes` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetClusterNodesRequest;
impl crate::StandardHttpRequest for crate::GetClusterNodesRequest {
type Response = std::vec::Vec<crate::RpcContactInfo>;
const METHOD: &'static str = "getClusterNodes";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getEpochSchedule` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetEpochScheduleRequest;
impl crate::StandardHttpRequest for crate::GetEpochScheduleRequest {
type Response = crate::RpcEpochSchedule;
const METHOD: &'static str = "getEpochSchedule";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getHealth` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetHealthRequest;
impl crate::StandardHttpRequest for crate::GetHealthRequest {
type Response = std::string::String;
const METHOD: &'static str = "getHealth";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getHighestSnapshotSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetHighestSnapshotSlotRequest;
impl crate::StandardHttpRequest for crate::GetHighestSnapshotSlotRequest {
type Response = crate::RpcSnapshotSlotInfo;
const METHOD: &'static str = "getHighestSnapshotSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getIdentity` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetIdentityRequest;
impl crate::StandardHttpRequest for crate::GetIdentityRequest {
type Response = crate::RpcIdentity;
const METHOD: &'static str = "getIdentity";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getLeaderSchedule` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetLeaderScheduleRequest {
/// Optional slot selecting the epoch whose schedule is requested.
pub slot: std::option::Option<u64>,
/// Optional identity and commitment options.
pub config: std::option::Option<crate::RpcLeaderScheduleConfig>,
}
impl crate::StandardHttpRequest for crate::GetLeaderScheduleRequest {
type Response = std::option::Option<crate::RpcLeaderSchedule>;
const METHOD: &'static str = "getLeaderSchedule";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let validation_result = config.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
let mut params = std::vec::Vec::new();
if let std::option::Option::Some(slot) = self.slot {
params.push(serde_json::Value::from(slot));
} else if self.config.is_some() {
params.push(serde_json::Value::Null);
}
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getMaxRetransmitSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetMaxRetransmitSlotRequest;
impl crate::StandardHttpRequest for crate::GetMaxRetransmitSlotRequest {
type Response = u64;
const METHOD: &'static str = "getMaxRetransmitSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getMaxShredInsertSlot` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetMaxShredInsertSlotRequest;
impl crate::StandardHttpRequest for crate::GetMaxShredInsertSlotRequest {
type Response = u64;
const METHOD: &'static str = "getMaxShredInsertSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSlot` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetSlotRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetSlotRequest {
type Response = u64;
const METHOD: &'static str = "getSlot";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSlotLeader` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetSlotLeaderRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetSlotLeaderRequest {
type Response = std::string::String;
const METHOD: &'static str = "getSlotLeader";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSlotLeaders` request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GetSlotLeadersRequest {
/// First slot whose leader is requested.
pub start_slot: u64,
/// Number of consecutive leaders requested.
pub limit: u64,
}
impl crate::StandardHttpRequest for crate::GetSlotLeadersRequest {
type Response = std::vec::Vec<std::string::String>;
const METHOD: &'static str = "getSlotLeaders";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if self.limit == 0 || self.limit > crate::MAX_SLOT_LEADER_COUNT {
return std::result::Result::Err(kb_core::Error::config(format!(
"getSlotLeaders limit must be between 1 and {}",
crate::MAX_SLOT_LEADER_COUNT
)));
}
return std::result::Result::Ok(std::vec![
serde_json::Value::from(self.start_slot),
serde_json::Value::from(self.limit),
]);
}
}
/// Typed `getVersion` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetVersionRequest;
impl crate::StandardHttpRequest for crate::GetVersionRequest {
type Response = crate::RpcVersionInfo;
const METHOD: &'static str = "getVersion";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getVoteAccounts` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetVoteAccountsRequest {
/// Optional vote-account, commitment and delinquency options.
pub config: std::option::Option<crate::RpcGetVoteAccountsConfig>,
}
impl crate::StandardHttpRequest for crate::GetVoteAccountsRequest {
type Response = crate::RpcVoteAccountStatus;
const METHOD: &'static str = "getVoteAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let validation_result = config.validate();
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn leader_schedule_uses_null_slot_placeholder_when_only_config_is_selected() {
let request = crate::GetLeaderScheduleRequest {
slot: std::option::Option::None,
config: std::option::Option::Some(crate::RpcLeaderScheduleConfig {
identity: std::option::Option::Some(pubkey(1)),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Processed),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[0], serde_json::Value::Null);
assert_eq!(params[1]["identity"], serde_json::Value::String(pubkey(1)));
let maximum = crate::GetSlotLeadersRequest {
start_slot: 1,
limit: crate::MAX_SLOT_LEADER_COUNT,
};
let too_many = crate::GetSlotLeadersRequest {
start_slot: 1,
limit: crate::MAX_SLOT_LEADER_COUNT + 1,
};
assert!(crate::StandardHttpRequest::params(&maximum).is_ok());
assert!(crate::StandardHttpRequest::params(&too_many).is_err());
}
#[test]
fn vote_account_options_are_independently_selectable() {
let request = crate::GetVoteAccountsRequest {
config: std::option::Option::Some(crate::RpcGetVoteAccountsConfig {
vote_pubkey: std::option::Option::Some(pubkey(2)),
commitment: std::option::Option::None,
keep_unstaked_delinquents: std::option::Option::Some(true),
delinquent_slot_distance: std::option::Option::Some(512),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert!(params[0].get("commitment").is_none());
assert_eq!(params[0]["keepUnstakedDelinquents"], serde_json::Value::Bool(true));
assert_eq!(params[0]["delinquentSlotDistance"], serde_json::Value::from(512_u64));
let vote_account =
match serde_json::from_value::<crate::RpcVoteAccountInfo>(serde_json::json!({
"votePubkey": pubkey(3),
"nodePubkey": pubkey(4),
"activatedStake": 1,
"commission": 5,
"inflationRewardsCommissionBps": 525,
"epochVoteAccount": true,
"epochCredits": [[1, 2, 1]],
"lastVote": 8,
"rootSlot": 7
})) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("vote account parsing failed: {error}"),
};
assert_eq!(vote_account.inflation_rewards_commission_bps, std::option::Option::Some(525));
}
#[test]
fn version_response_accepts_kebab_case_wire_fields() {
let value = serde_json::json!({ "solana-core": "4.0.0", "feature-set": 123 });
let parsed = match serde_json::from_value::<crate::RpcVersionInfo>(value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("version parsing failed: {error}"),
};
assert_eq!(parsed.solana_core, "4.0.0");
assert_eq!(parsed.feature_set, std::option::Option::Some(123));
}
}

View File

@@ -0,0 +1,305 @@
// file: kb-onchain-transport/src/standard_http_economics.rs
// version: 2
//! Configurable standard inflation, supply and stake-economics HTTP requests.
/// Inflation governor parameters.
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationGovernor {
/// Initial inflation rate.
pub initial: f64,
/// Terminal inflation rate.
pub terminal: f64,
/// Annual taper rate.
pub taper: f64,
/// Foundation allocation rate.
pub foundation: f64,
/// Foundation allocation term in years.
pub foundation_term: f64,
}
/// Inflation rates for the current epoch.
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationRate {
/// Total inflation rate.
pub total: f64,
/// Validator inflation rate.
pub validator: f64,
/// Foundation inflation rate.
pub foundation: f64,
/// Epoch represented by the rates.
pub epoch: u64,
}
/// Inflation reward credited to one requested address.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationReward {
/// Reward epoch.
pub epoch: u64,
/// First effective slot of the rewarded epoch.
pub effective_slot: u64,
/// Reward amount in lamports.
pub amount: u64,
/// Account balance after the reward.
pub post_balance: u64,
/// Legacy vote commission percentage when applicable.
pub commission: std::option::Option<u8>,
/// Vote commission in basis points when exposed by the node.
#[serde(default, skip_serializing_if = "std::option::Option::is_none")]
pub commission_bps: std::option::Option<u16>,
}
/// Current lamport supply breakdown.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSupply {
/// Total lamport supply.
pub total: u64,
/// Circulating lamport supply.
pub circulating: u64,
/// Non-circulating lamport supply.
pub non_circulating: u64,
/// Non-circulating account list when requested.
#[serde(default)]
pub non_circulating_accounts: std::vec::Vec<std::string::String>,
}
/// Optional commitment accepted by `getInflationGovernor`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcInflationGovernorConfig {
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
}
/// Epoch and contextual options accepted by `getInflationReward`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcEpochConfig {
/// Optional reward epoch.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub epoch: std::option::Option<u64>,
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional minimum context slot.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub min_context_slot: std::option::Option<u64>,
}
/// Options accepted by `getSupply`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSupplyConfig {
/// Optional commitment level.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub commitment: std::option::Option<crate::RpcCommitmentLevel>,
/// Optional omission of the potentially large non-circulating account list.
#[serde(skip_serializing_if = "std::option::Option::is_none")]
pub exclude_non_circulating_accounts_list: std::option::Option<bool>,
}
/// Typed `getInflationGovernor` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetInflationGovernorRequest {
/// Optional commitment configuration.
pub config: std::option::Option<crate::RpcInflationGovernorConfig>,
}
impl crate::StandardHttpRequest for crate::GetInflationGovernorRequest {
type Response = crate::RpcInflationGovernor;
const METHOD: &'static str = "getInflationGovernor";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getInflationRate` request.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct GetInflationRateRequest;
impl crate::StandardHttpRequest for crate::GetInflationRateRequest {
type Response = crate::RpcInflationRate;
const METHOD: &'static str = "getInflationRate";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getInflationReward` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetInflationRewardRequest {
/// Account public keys whose rewards are requested, in response order.
pub addresses: std::vec::Vec<std::string::String>,
/// Optional epoch, commitment and minimum-context options.
pub config: std::option::Option<crate::RpcEpochConfig>,
}
impl crate::StandardHttpRequest for crate::GetInflationRewardRequest {
type Response = std::vec::Vec<std::option::Option<crate::RpcInflationReward>>;
const METHOD: &'static str = "getInflationReward";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_pubkey_list(&self.addresses, "getInflationReward address", usize::MAX);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let addresses = match crate::serialize_parameter(Self::METHOD, &self.addresses) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut params = std::vec![addresses];
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
/// Typed `getStakeMinimumDelegation` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetStakeMinimumDelegationRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetStakeMinimumDelegationRequest {
type Response = crate::RpcResponse<u64>;
const METHOD: &'static str = "getStakeMinimumDelegation";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getSupply` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetSupplyRequest {
/// Optional commitment and non-circulating-list options.
pub config: std::option::Option<crate::RpcSupplyConfig>,
}
impl crate::StandardHttpRequest for crate::GetSupplyRequest {
type Response = crate::RpcResponse<crate::RpcSupply>;
const METHOD: &'static str = "getSupply";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn supply_distinguishes_omitted_and_explicit_false_option() {
let omitted = crate::GetSupplyRequest {
config: std::option::Option::Some(crate::RpcSupplyConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
exclude_non_circulating_accounts_list: std::option::Option::None,
}),
};
let explicit = crate::GetSupplyRequest {
config: std::option::Option::Some(crate::RpcSupplyConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
exclude_non_circulating_accounts_list: std::option::Option::Some(false),
}),
};
let omitted_params = match crate::StandardHttpRequest::params(&omitted) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
let explicit_params = match crate::StandardHttpRequest::params(&explicit) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert!(omitted_params[0].get("excludeNonCirculatingAccountsList").is_none());
assert_eq!(
explicit_params[0]["excludeNonCirculatingAccountsList"],
serde_json::Value::Bool(false)
);
}
#[test]
fn inflation_reward_preserves_epoch_commitment_and_minimum_context() {
let request = crate::GetInflationRewardRequest {
addresses: std::vec![pubkey(1), pubkey(2)],
config: std::option::Option::Some(crate::RpcEpochConfig {
epoch: std::option::Option::Some(44),
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed),
min_context_slot: std::option::Option::Some(99),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["epoch"], serde_json::Value::from(44_u64));
assert_eq!(params[1]["commitment"], serde_json::Value::String("confirmed".to_string()));
assert_eq!(params[1]["minContextSlot"], serde_json::Value::from(99_u64));
let reward = match serde_json::from_value::<crate::RpcInflationReward>(serde_json::json!({
"epoch": 44,
"effectiveSlot": 100,
"amount": 200,
"postBalance": 300,
"commission": 5,
"commissionBps": 575
})) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("inflation reward parsing failed: {error}"),
};
assert_eq!(reward.commission, std::option::Option::Some(5));
assert_eq!(reward.commission_bps, std::option::Option::Some(575));
let empty = crate::GetInflationRewardRequest {
addresses: std::vec::Vec::new(),
config: std::option::Option::None,
};
let empty_params = match crate::StandardHttpRequest::params(&empty) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("empty params failed: {error}"),
};
assert_eq!(empty_params, std::vec![serde_json::json!([])]);
}
}

View File

@@ -0,0 +1,241 @@
// file: kb-onchain-transport/src/standard_http_tokens.rs
// version: 2
//! Configurable standard SPL Token-oriented Solana HTTP JSON-RPC requests.
fn pubkey_with_optional_commitment_params(
method: &str,
address: &str,
field: &str,
config: &std::option::Option<crate::RpcCommitmentConfig>,
) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result = crate::validate_solana_pubkey_text(address, field);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut params = std::vec![serde_json::Value::String(address.to_string())];
if let std::option::Option::Some(config) = config {
let config_value = match crate::serialize_parameter(method, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
fn token_accounts_query_params(
method: &str,
authority: &str,
field: &str,
filter: &crate::RpcTokenAccountsFilter,
config: &std::option::Option<crate::RpcAccountInfoConfig>,
) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let authority_result = crate::validate_solana_pubkey_text(authority, field);
if let std::result::Result::Err(error) = authority_result {
return std::result::Result::Err(error);
}
let filter_result = filter.validate();
if let std::result::Result::Err(error) = filter_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(config) = config {
let config_result = config.validate();
if let std::result::Result::Err(error) = config_result {
return std::result::Result::Err(error);
}
}
let filter_value = match crate::serialize_parameter(method, filter) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut params = std::vec![serde_json::Value::String(authority.to_string()), filter_value,];
if let std::option::Option::Some(config) = config {
let config_value = match crate::serialize_parameter(method, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(config_value);
}
return std::result::Result::Ok(params);
}
/// Typed `getTokenAccountBalance` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenAccountBalanceRequest {
/// Token account public key.
pub address: std::string::String,
/// Optional commitment option.
pub config: std::option::Option<crate::RpcCommitmentConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenAccountBalanceRequest {
type Response = crate::RpcResponse<crate::RpcTokenAmount>;
const METHOD: &'static str = "getTokenAccountBalance";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::pubkey_with_optional_commitment_params(
Self::METHOD,
&self.address,
"getTokenAccountBalance address",
&self.config,
);
}
}
/// Typed `getTokenAccountsByDelegate` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenAccountsByDelegateRequest {
/// Delegate public key.
pub delegate: std::string::String,
/// Mint or Token Program selector.
pub filter: crate::RpcTokenAccountsFilter,
/// Optional account representation and context options.
pub config: std::option::Option<crate::RpcAccountInfoConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenAccountsByDelegateRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcKeyedAccount>>;
const METHOD: &'static str = "getTokenAccountsByDelegate";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::token_accounts_query_params(
Self::METHOD,
&self.delegate,
"getTokenAccountsByDelegate delegate",
&self.filter,
&self.config,
);
}
}
/// Typed `getTokenAccountsByOwner` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenAccountsByOwnerRequest {
/// Owner public key.
pub owner: std::string::String,
/// Mint or Token Program selector.
pub filter: crate::RpcTokenAccountsFilter,
/// Optional account representation and context options.
pub config: std::option::Option<crate::RpcAccountInfoConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenAccountsByOwnerRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcKeyedAccount>>;
const METHOD: &'static str = "getTokenAccountsByOwner";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::token_accounts_query_params(
Self::METHOD,
&self.owner,
"getTokenAccountsByOwner owner",
&self.filter,
&self.config,
);
}
}
/// Typed `getTokenLargestAccounts` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenLargestAccountsRequest {
/// Mint public key.
pub mint: std::string::String,
/// Optional commitment option.
pub config: std::option::Option<crate::RpcCommitmentConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenLargestAccountsRequest {
type Response = crate::RpcResponse<std::vec::Vec<crate::RpcTokenAccountBalance>>;
const METHOD: &'static str = "getTokenLargestAccounts";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::pubkey_with_optional_commitment_params(
Self::METHOD,
&self.mint,
"getTokenLargestAccounts mint",
&self.config,
);
}
}
/// Typed `getTokenSupply` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetTokenSupplyRequest {
/// Mint public key.
pub mint: std::string::String,
/// Optional commitment option.
pub config: std::option::Option<crate::RpcCommitmentConfig>,
}
impl crate::StandardHttpRequest for crate::GetTokenSupplyRequest {
type Response = crate::RpcResponse<crate::RpcTokenAmount>;
const METHOD: &'static str = "getTokenSupply";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
return crate::standard_http_tokens::pubkey_with_optional_commitment_params(
Self::METHOD,
&self.mint,
"getTokenSupply mint",
&self.config,
);
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn token_owner_query_preserves_filter_and_independent_account_options() {
let request = crate::GetTokenAccountsByOwnerRequest {
owner: pubkey(1),
filter: crate::RpcTokenAccountsFilter::ProgramId(pubkey(2)),
config: std::option::Option::Some(crate::RpcAccountInfoConfig {
encoding: std::option::Option::Some(crate::RpcAccountEncoding::Base64Zstd),
data_slice: std::option::Option::None,
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Processed),
min_context_slot: std::option::Option::Some(42),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1], serde_json::json!({ "programId": pubkey(2) }));
assert_eq!(params[2]["encoding"], serde_json::Value::String("base64+zstd".to_string()));
assert_eq!(params[2]["commitment"], serde_json::Value::String("processed".to_string()));
}
#[test]
fn token_balance_omits_unselected_options() {
let request = crate::GetTokenAccountBalanceRequest {
address: pubkey(3),
config: std::option::Option::None,
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params, std::vec![serde_json::Value::String(pubkey(3))]);
let configured = crate::GetTokenSupplyRequest {
mint: pubkey(4),
config: std::option::Option::Some(crate::RpcCommitmentConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
}),
};
let configured_params = match crate::StandardHttpRequest::params(&configured) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(configured_params[1], serde_json::json!({ "commitment": "finalized" }));
assert!(configured_params[1].get("minContextSlot").is_none());
}
}

View File

@@ -0,0 +1,155 @@
// file: kb-onchain-transport/src/standard_http_transactions.rs
// version: 3
//! Configurable standard transaction-oriented Solana HTTP JSON-RPC requests.
/// Prioritization fee observed for one recent slot.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcPrioritizationFee {
/// Slot from which the fee sample was retained.
pub slot: u64,
/// Minimum compute-unit price in micro-lamports for the requested writable set.
pub prioritization_fee: u64,
}
/// Typed `getRecentPrioritizationFees` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetRecentPrioritizationFeesRequest {
/// Optional writable account set. `None` omits the parameter; `Some([])` sends an explicit empty set.
pub locked_writable_accounts: std::option::Option<std::vec::Vec<std::string::String>>,
}
impl crate::StandardHttpRequest for crate::GetRecentPrioritizationFeesRequest {
type Response = std::vec::Vec<crate::RpcPrioritizationFee>;
const METHOD: &'static str = "getRecentPrioritizationFees";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(accounts) = &self.locked_writable_accounts {
let validation_result = crate::validate_pubkey_list(
accounts,
"getRecentPrioritizationFees writable account",
crate::MAX_PRIORITIZATION_FEE_ACCOUNT_COUNT,
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let value = match crate::serialize_parameter(Self::METHOD, accounts) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `getTransactionCount` request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct GetTransactionCountRequest {
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::GetTransactionCountRequest {
type Response = u64;
const METHOD: &'static str = "getTransactionCount";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![value]);
}
return std::result::Result::Ok(std::vec::Vec::new());
}
}
/// Typed `isBlockhashValid` request.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IsBlockhashValidRequest {
/// Base58 blockhash being checked.
pub blockhash: std::string::String,
/// Optional commitment and minimum-context options.
pub config: std::option::Option<crate::RpcContextConfig>,
}
impl crate::StandardHttpRequest for crate::IsBlockhashValidRequest {
type Response = crate::RpcResponse<bool>;
const METHOD: &'static str = "isBlockhashValid";
fn params(&self) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_solana_hash_text(&self.blockhash, "isBlockhashValid blockhash");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut params = std::vec![serde_json::Value::String(self.blockhash.clone())];
if let std::option::Option::Some(config) = &self.config {
let value = match crate::serialize_parameter(Self::METHOD, config) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
params.push(value);
}
return std::result::Result::Ok(params);
}
}
#[cfg(test)]
mod tests {
fn pubkey(seed: u8) -> std::string::String {
return bs58::encode([seed; 32]).into_string();
}
#[test]
fn prioritization_fee_request_distinguishes_omitted_and_explicit_empty_sets() {
let omitted = crate::GetRecentPrioritizationFeesRequest {
locked_writable_accounts: std::option::Option::None,
};
let explicit = crate::GetRecentPrioritizationFeesRequest {
locked_writable_accounts: std::option::Option::Some(std::vec::Vec::new()),
};
let omitted_params = match crate::StandardHttpRequest::params(&omitted) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("unexpected omitted params error: {error}"),
};
let explicit_params = match crate::StandardHttpRequest::params(&explicit) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("unexpected explicit params error: {error}"),
};
assert_eq!(omitted_params, std::vec::Vec::<serde_json::Value>::new());
assert_eq!(explicit_params, std::vec![serde_json::json!([])]);
}
#[test]
fn prioritization_fee_request_enforces_official_account_bound() {
let request = crate::GetRecentPrioritizationFeesRequest {
locked_writable_accounts: std::option::Option::Some(
(0_u16..129_u16).map(|value| return pubkey(value as u8)).collect(),
),
};
assert!(crate::StandardHttpRequest::params(&request).is_err());
}
#[test]
fn blockhash_validity_preserves_minimum_context_slot() {
let request = crate::IsBlockhashValidRequest {
blockhash: pubkey(9),
config: std::option::Option::Some(crate::RpcContextConfig {
commitment: std::option::Option::Some(crate::RpcCommitmentLevel::Finalized),
min_context_slot: std::option::Option::Some(123),
}),
};
let params = match crate::StandardHttpRequest::params(&request) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => panic!("params failed: {error}"),
};
assert_eq!(params[1]["minContextSlot"], serde_json::Value::from(123_u64));
}
}

View File

@@ -0,0 +1,670 @@
// file: kb-onchain-transport/src/standard_methods.rs
// version: 4
//! Canonical inventory of standard Solana HTTP and WebSocket JSON-RPC methods.
/// Strongest implementation contract exposed for one standard RPC method.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StandardRpcContract {
/// The crate exposes a dedicated request/result adapter in addition to raw JSON transport.
TypedAdapter,
/// The method is explicitly registered and callable through validated raw JSON transport.
RawJson,
}
impl crate::StandardRpcContract {
/// Returns the stable matrix code for this contract.
pub const fn code(self) -> &'static str {
return match self {
Self::TypedAdapter => "typed_adapter",
Self::RawJson => "raw_json",
};
}
}
/// Local category used to group standard Solana HTTP methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StandardHttpCategory {
/// Account state and account ownership reads.
Accounts,
/// SPL Token account and mint reads.
Tokens,
/// Transaction, signature, fee, simulation and submission methods.
Transactions,
/// Block, ledger and performance methods.
Blocks,
/// Cluster, node, epoch, slot and validator methods.
Cluster,
/// Inflation, supply and stake-economics methods.
Economics,
}
impl crate::StandardHttpCategory {
/// Returns the stable matrix code for this category.
pub const fn code(self) -> &'static str {
return match self {
Self::Accounts => "accounts",
Self::Tokens => "tokens",
Self::Transactions => "transactions",
Self::Blocks => "blocks",
Self::Cluster => "cluster",
Self::Economics => "economics",
};
}
}
/// One standard Solana HTTP JSON-RPC method specification.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StandardHttpMethodSpec {
/// Exact JSON-RPC method name.
pub method: &'static str,
/// Local inventory category.
pub category: crate::StandardHttpCategory,
/// Strongest contract currently exposed by `kb-onchain-transport`.
pub contract: crate::StandardRpcContract,
}
impl crate::StandardHttpMethodSpec {
/// Returns the local HTTP routing class for this method.
pub fn method_class(&self) -> crate::HttpMethodClass {
return match self.method {
"requestAirdrop" | "sendTransaction" => crate::HttpMethodClass::SendTransaction,
"getBlock"
| "getBlocks"
| "getBlocksWithLimit"
| "getProgramAccounts"
| "getSignaturesForAddress"
| "getTransaction"
| "simulateTransaction" => crate::HttpMethodClass::HeavyRead,
_ => crate::HttpMethodClass::GeneralRpc,
};
}
}
/// Every standard Solana HTTP JSON-RPC method documented by the canonical RPC reference.
pub const STANDARD_HTTP_METHODS: [crate::StandardHttpMethodSpec; 52] = [
crate::StandardHttpMethodSpec {
method: "getAccountInfo",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBalance",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getLargestAccounts",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMinimumBalanceForRentExemption",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMultipleAccounts",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getProgramAccounts",
category: crate::StandardHttpCategory::Accounts,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenAccountBalance",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenAccountsByDelegate",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenAccountsByOwner",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenLargestAccounts",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTokenSupply",
category: crate::StandardHttpCategory::Tokens,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getFeeForMessage",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getLatestBlockhash",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getRecentPrioritizationFees",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSignaturesForAddress",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSignatureStatuses",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTransaction",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getTransactionCount",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "isBlockhashValid",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "requestAirdrop",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "sendTransaction",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "simulateTransaction",
category: crate::StandardHttpCategory::Transactions,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlock",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockCommitment",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockHeight",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockProduction",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlocks",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlocksWithLimit",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getBlockTime",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getFirstAvailableBlock",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getRecentPerformanceSamples",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "minimumLedgerSlot",
category: crate::StandardHttpCategory::Blocks,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getClusterNodes",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getEpochInfo",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getEpochSchedule",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getGenesisHash",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getHealth",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getHighestSnapshotSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getIdentity",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getLeaderSchedule",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMaxRetransmitSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getMaxShredInsertSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSlot",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSlotLeader",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSlotLeaders",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getVersion",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getVoteAccounts",
category: crate::StandardHttpCategory::Cluster,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getInflationGovernor",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getInflationRate",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getInflationReward",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getStakeMinimumDelegation",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
crate::StandardHttpMethodSpec {
method: "getSupply",
category: crate::StandardHttpCategory::Economics,
contract: crate::StandardRpcContract::TypedAdapter,
},
];
/// Returns the canonical specification for one exact standard HTTP method name.
pub fn standard_http_method(
method: &str,
) -> std::option::Option<&'static crate::StandardHttpMethodSpec> {
return crate::STANDARD_HTTP_METHODS.iter().find(|entry| return entry.method == method);
}
/// Stability of one standard Solana WebSocket subscription surface.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StandardWsStability {
/// Stable standard PubSub method.
Stable,
/// Method documented as unstable and potentially gated by validator flags.
Unstable,
}
impl crate::StandardWsStability {
/// Returns the stable matrix code for this stability class.
pub const fn code(self) -> &'static str {
return match self {
Self::Stable => "stable",
Self::Unstable => "unstable",
};
}
}
/// One standard Solana WebSocket subscribe/unsubscribe pair.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StandardWsSubscriptionSpec {
/// Exact subscribe method name.
pub subscribe_method: &'static str,
/// Exact unsubscribe method name.
pub unsubscribe_method: &'static str,
/// Exact notification method name emitted by the server.
pub notification_method: &'static str,
/// Stability declared by the canonical Solana RPC documentation.
pub stability: crate::StandardWsStability,
/// Strongest typed request contract exposed by `kb-onchain-transport`.
pub request_contract: crate::StandardRpcContract,
/// Strongest typed notification contract exposed by `kb-onchain-transport`.
pub notification_contract: crate::StandardRpcContract,
/// Whether the pair is supported by the reusable persistent session runtime.
pub persistent_runtime: bool,
}
/// Every standard Solana WebSocket subscription pair documented by the canonical RPC reference.
pub const STANDARD_WS_SUBSCRIPTIONS: [crate::StandardWsSubscriptionSpec; 9] = [
crate::StandardWsSubscriptionSpec {
subscribe_method: "accountSubscribe",
unsubscribe_method: "accountUnsubscribe",
notification_method: "accountNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "blockSubscribe",
unsubscribe_method: "blockUnsubscribe",
notification_method: "blockNotification",
stability: crate::StandardWsStability::Unstable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "logsSubscribe",
unsubscribe_method: "logsUnsubscribe",
notification_method: "logsNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "programSubscribe",
unsubscribe_method: "programUnsubscribe",
notification_method: "programNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "rootSubscribe",
unsubscribe_method: "rootUnsubscribe",
notification_method: "rootNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "signatureSubscribe",
unsubscribe_method: "signatureUnsubscribe",
notification_method: "signatureNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "slotSubscribe",
unsubscribe_method: "slotUnsubscribe",
notification_method: "slotNotification",
stability: crate::StandardWsStability::Stable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "slotsUpdatesSubscribe",
unsubscribe_method: "slotsUpdatesUnsubscribe",
notification_method: "slotsUpdatesNotification",
stability: crate::StandardWsStability::Unstable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
crate::StandardWsSubscriptionSpec {
subscribe_method: "voteSubscribe",
unsubscribe_method: "voteUnsubscribe",
notification_method: "voteNotification",
stability: crate::StandardWsStability::Unstable,
request_contract: crate::StandardRpcContract::TypedAdapter,
notification_contract: crate::StandardRpcContract::TypedAdapter,
persistent_runtime: true,
},
];
/// Returns the canonical subscription specification matching a subscribe or unsubscribe method.
pub fn standard_ws_subscription(
method: &str,
) -> std::option::Option<&'static crate::StandardWsSubscriptionSpec> {
return crate::STANDARD_WS_SUBSCRIPTIONS.iter().find(|entry| {
return entry.subscribe_method == method || entry.unsubscribe_method == method;
});
}
#[cfg(test)]
mod tests {
#[test]
fn standard_http_inventory_is_exact_unique_and_classified() {
assert_eq!(crate::STANDARD_HTTP_METHODS.len(), 52);
let mut names = std::collections::BTreeSet::new();
let mut typed_count = 0_usize;
for method in &crate::STANDARD_HTTP_METHODS {
assert!(names.insert(method.method));
if method.contract == crate::StandardRpcContract::TypedAdapter {
typed_count = typed_count.saturating_add(1);
}
assert_eq!(
crate::standard_http_method(method.method),
std::option::Option::Some(method)
);
}
assert_eq!(typed_count, 52);
}
#[test]
fn configurable_request_registry_covers_the_former_thirty_eight_raw_methods() {
fn method<Request: crate::StandardHttpRequest>() -> &'static str {
return <Request as crate::StandardHttpRequest>::METHOD;
}
let methods = [
method::<crate::GetLargestAccountsRequest>(),
method::<crate::GetMultipleAccountsRequest>(),
method::<crate::GetProgramAccountsRequest>(),
method::<crate::GetTokenAccountBalanceRequest>(),
method::<crate::GetTokenAccountsByDelegateRequest>(),
method::<crate::GetTokenAccountsByOwnerRequest>(),
method::<crate::GetTokenLargestAccountsRequest>(),
method::<crate::GetTokenSupplyRequest>(),
method::<crate::GetRecentPrioritizationFeesRequest>(),
method::<crate::GetTransactionCountRequest>(),
method::<crate::IsBlockhashValidRequest>(),
method::<crate::GetBlockRequest>(),
method::<crate::GetBlockCommitmentRequest>(),
method::<crate::GetBlockProductionRequest>(),
method::<crate::GetBlocksRequest>(),
method::<crate::GetBlocksWithLimitRequest>(),
method::<crate::GetBlockTimeRequest>(),
method::<crate::GetFirstAvailableBlockRequest>(),
method::<crate::GetRecentPerformanceSamplesRequest>(),
method::<crate::MinimumLedgerSlotRequest>(),
method::<crate::GetClusterNodesRequest>(),
method::<crate::GetEpochScheduleRequest>(),
method::<crate::GetHealthRequest>(),
method::<crate::GetHighestSnapshotSlotRequest>(),
method::<crate::GetIdentityRequest>(),
method::<crate::GetLeaderScheduleRequest>(),
method::<crate::GetMaxRetransmitSlotRequest>(),
method::<crate::GetMaxShredInsertSlotRequest>(),
method::<crate::GetSlotRequest>(),
method::<crate::GetSlotLeaderRequest>(),
method::<crate::GetSlotLeadersRequest>(),
method::<crate::GetVersionRequest>(),
method::<crate::GetVoteAccountsRequest>(),
method::<crate::GetInflationGovernorRequest>(),
method::<crate::GetInflationRateRequest>(),
method::<crate::GetInflationRewardRequest>(),
method::<crate::GetStakeMinimumDelegationRequest>(),
method::<crate::GetSupplyRequest>(),
];
assert_eq!(methods.len(), 38);
let mut unique = std::collections::BTreeSet::new();
for method_name in methods {
assert!(unique.insert(method_name));
let specification = match crate::standard_http_method(method_name) {
std::option::Option::Some(specification) => specification,
std::option::Option::None => panic!("typed method absent from registry"),
};
assert_eq!(specification.contract, crate::StandardRpcContract::TypedAdapter);
}
}
#[test]
fn standard_ws_inventory_contains_nine_pairs_and_eighteen_unique_methods() {
assert_eq!(crate::STANDARD_WS_SUBSCRIPTIONS.len(), 9);
let mut names = std::collections::BTreeSet::new();
let mut unstable_count = 0_usize;
for subscription in &crate::STANDARD_WS_SUBSCRIPTIONS {
assert!(names.insert(subscription.subscribe_method));
assert!(names.insert(subscription.unsubscribe_method));
if subscription.stability == crate::StandardWsStability::Unstable {
unstable_count = unstable_count.saturating_add(1);
}
assert_eq!(subscription.request_contract, crate::StandardRpcContract::TypedAdapter);
assert_eq!(
subscription.notification_contract,
crate::StandardRpcContract::TypedAdapter
);
assert!(subscription.persistent_runtime);
assert_eq!(
crate::standard_ws_subscription(subscription.subscribe_method),
std::option::Option::Some(subscription)
);
assert_eq!(
crate::standard_ws_subscription(subscription.unsubscribe_method),
std::option::Option::Some(subscription)
);
}
assert_eq!(names.len(), 18);
assert_eq!(unstable_count, 3);
}
#[test]
fn standard_rpc_matrix_matches_compiled_inventory() {
let raw = include_str!("../../docs/SOLANA_STANDARD_RPC_MATRIX.json");
let parsed = match serde_json::from_str::<serde_json::Value>(raw) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("rpc matrix parsing failed: {error}"),
};
let http_methods = match parsed.get("http_methods").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix http_methods missing"),
};
let ws_subscriptions =
match parsed.get("ws_subscriptions").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix ws_subscriptions missing"),
};
assert_eq!(http_methods.len(), crate::STANDARD_HTTP_METHODS.len());
assert_eq!(ws_subscriptions.len(), crate::STANDARD_WS_SUBSCRIPTIONS.len());
for method in &crate::STANDARD_HTTP_METHODS {
let matrix_entry = http_methods.iter().find(|entry| {
return entry.get("method").and_then(serde_json::Value::as_str)
== std::option::Option::Some(method.method);
});
let matrix_entry = match matrix_entry {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix HTTP method missing"),
};
assert_eq!(
matrix_entry.get("category").and_then(serde_json::Value::as_str),
std::option::Option::Some(method.category.code())
);
assert_eq!(
matrix_entry.get("contract").and_then(serde_json::Value::as_str),
std::option::Option::Some(method.contract.code())
);
}
for subscription in &crate::STANDARD_WS_SUBSCRIPTIONS {
let matrix_entry = ws_subscriptions.iter().find(|entry| {
return entry.get("subscribe_method").and_then(serde_json::Value::as_str)
== std::option::Option::Some(subscription.subscribe_method);
});
let matrix_entry = match matrix_entry {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("rpc matrix WS subscription missing"),
};
assert_eq!(
matrix_entry.get("unsubscribe_method").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.unsubscribe_method)
);
assert_eq!(
matrix_entry.get("notification_method").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.notification_method)
);
assert_eq!(
matrix_entry.get("stability").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.stability.code())
);
assert_eq!(
matrix_entry.get("request_contract").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.request_contract.code())
);
assert_eq!(
matrix_entry.get("notification_contract").and_then(serde_json::Value::as_str),
std::option::Option::Some(subscription.notification_contract.code())
);
assert_eq!(
matrix_entry.get("persistent_runtime").and_then(serde_json::Value::as_bool),
std::option::Option::Some(subscription.persistent_runtime)
);
}
}
}

View File

@@ -0,0 +1,77 @@
// file: kb-onchain-transport/src/validation.rs
// version: 3
//! Shared validation helpers for Solana RPC addresses, hashes and signatures.
/// Validates one base58 Solana transaction signature.
pub fn validate_transaction_signature_text(value: &str, field_name: &str) -> kb_core::Result<()> {
return crate::validation::validate_base58_length(value, field_name, 64);
}
/// Validates one base58 Solana public key or account address.
pub fn validate_solana_pubkey_text(value: &str, field_name: &str) -> kb_core::Result<()> {
return crate::validation::validate_base58_length(value, field_name, 32);
}
/// Validates one base58 Solana blockhash or genesis hash.
pub fn validate_solana_hash_text(value: &str, field_name: &str) -> kb_core::Result<()> {
return crate::validation::validate_base58_length(value, field_name, 32);
}
fn validate_base58_length(
value: &str,
field_name: &str,
expected_length: usize,
) -> kb_core::Result<()> {
if value.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field_name} must not be empty"
)));
}
let decode_result = bs58::decode(value).into_vec();
let decoded = match decode_result {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field_name} is not valid base58: {error}"
)));
},
};
if decoded.len() != expected_length {
return std::result::Result::Err(kb_core::Error::config(format!(
"{field_name} must decode to {expected_length} bytes"
)));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
#[test]
fn signature_validation_accepts_sixty_four_bytes() {
let signature = bs58::encode([7_u8; 64]).into_string();
let result = crate::validate_transaction_signature_text(&signature, "signature");
assert!(result.is_ok());
}
#[test]
fn pubkey_validation_accepts_thirty_two_bytes() {
let pubkey = bs58::encode([9_u8; 32]).into_string();
let result = crate::validate_solana_pubkey_text(&pubkey, "pubkey");
assert!(result.is_ok());
}
#[test]
fn hash_validation_accepts_thirty_two_bytes() {
let hash = bs58::encode([10_u8; 32]).into_string();
let result = crate::validate_solana_hash_text(&hash, "hash");
assert!(result.is_ok());
}
#[test]
fn signature_validation_rejects_public_key_length() {
let pubkey = bs58::encode([11_u8; 32]).into_string();
let result = crate::validate_transaction_signature_text(&pubkey, "signature");
assert!(result.is_err());
}
}

0
results.txt Normal file
View File