Files
khadhroony-bot3/migration/khadhroony-bot2-reference/kb_rpc/src/standard_http.rs
2026-07-23 16:37:12 +02:00

500 lines
19 KiB
Rust

// file: kb_rpc/src/standard_http.rs
// version: 2
//! 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::constants::MAX_MEMCMP_BASE58_LENGTH {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base58 value must not exceed {} characters",
crate::constants::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::constants::MAX_MEMCMP_BASE64_LENGTH {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp base64 value must not exceed {} characters",
crate::constants::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::constants::MAX_MEMCMP_DECODED_BYTES {
return std::result::Result::Err(kb_core::Error::config(format!(
"memcmp value must not exceed {} decoded bytes",
crate::constants::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());
}
}