0.1.0
This commit is contained in:
499
migration/khadhroony-bot2-reference/kb_rpc/src/standard_http.rs
Normal file
499
migration/khadhroony-bot2-reference/kb_rpc/src/standard_http.rs
Normal file
@@ -0,0 +1,499 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user