v0.1.0-pre.024
This commit is contained in:
@@ -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 d’endpoints, des clients et pools HTTP, des validateurs et des méthodes HTTP Solana standard.
|
||||
- Migration des adaptateurs RPC d’exé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`.
|
||||
|
||||
@@ -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 l’ancienne `kb_rpc`.
|
||||
- [x] Renommer structurellement la crate.
|
||||
- [x] Porter les contrats JSON-RPC, rôles d’endpoints, validation, clients/pools HTTP et méthodes HTTP standard.
|
||||
- [ ] Porter WebSocket, sessions et pools d’abonnements.
|
||||
- [ ] Porter l’acquisition 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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
22
kb-onchain-transport/src/client.rs
Normal file
22
kb-onchain-transport/src/client.rs
Normal 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>>;
|
||||
}
|
||||
41
kb-onchain-transport/src/constants.rs
Normal file
41
kb-onchain-transport/src/constants.rs
Normal 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;
|
||||
190
kb-onchain-transport/src/endpoint_role.rs
Normal file
190
kb-onchain-transport/src/endpoint_role.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
2703
kb-onchain-transport/src/execution_rpc.rs
Normal file
2703
kb-onchain-transport/src/execution_rpc.rs
Normal file
File diff suppressed because it is too large
Load Diff
383
kb-onchain-transport/src/http_client.rs
Normal file
383
kb-onchain-transport/src/http_client.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
279
kb-onchain-transport/src/http_pool.rs
Normal file
279
kb-onchain-transport/src/http_pool.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
315
kb-onchain-transport/src/json_rpc.rs
Normal file
315
kb-onchain-transport/src/json_rpc.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
499
kb-onchain-transport/src/standard_http.rs
Normal file
499
kb-onchain-transport/src/standard_http.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
212
kb-onchain-transport/src/standard_http_accounts.rs
Normal file
212
kb-onchain-transport/src/standard_http_accounts.rs
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
548
kb-onchain-transport/src/standard_http_blocks.rs
Normal file
548
kb-onchain-transport/src/standard_http_blocks.rs
Normal 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)]);
|
||||
}
|
||||
}
|
||||
505
kb-onchain-transport/src/standard_http_cluster.rs
Normal file
505
kb-onchain-transport/src/standard_http_cluster.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
305
kb-onchain-transport/src/standard_http_economics.rs
Normal file
305
kb-onchain-transport/src/standard_http_economics.rs
Normal 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!([])]);
|
||||
}
|
||||
}
|
||||
241
kb-onchain-transport/src/standard_http_tokens.rs
Normal file
241
kb-onchain-transport/src/standard_http_tokens.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
155
kb-onchain-transport/src/standard_http_transactions.rs
Normal file
155
kb-onchain-transport/src/standard_http_transactions.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
670
kb-onchain-transport/src/standard_methods.rs
Normal file
670
kb-onchain-transport/src/standard_methods.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
kb-onchain-transport/src/validation.rs
Normal file
77
kb-onchain-transport/src/validation.rs
Normal 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
0
results.txt
Normal file
Reference in New Issue
Block a user