Files
khadhroony-bot3/ks-onchain-transport/src/execution_rpc.rs
2026-08-09 19:34:08 +02:00

2705 lines
108 KiB
Rust

// file: ks-onchain-transport/src/execution_rpc.rs
// version: 11
//! Typed Solana JSON-RPC adapters used by execution orchestration.
use base64::Engine; // rust-rules: trait-import
/// Commitment level accepted by execution-oriented RPC methods.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RpcCommitmentLevel {
/// The node's most recent processed state.
Processed,
/// A supermajority-confirmed state.
Confirmed,
/// The strongest finalized state.
Finalized,
}
impl crate::RpcCommitmentLevel {
/// Returns the wire value expected by Solana JSON-RPC.
pub fn as_str(&self) -> &'static str {
return match self {
Self::Processed => "processed",
Self::Confirmed => "confirmed",
Self::Finalized => "finalized",
};
}
}
/// Standard context attached to Solana RPC responses.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcResponseContext {
/// Slot used by the RPC node to answer the request.
pub slot: u64,
/// Optional node API version.
#[serde(default)]
pub api_version: std::option::Option<std::string::String>,
}
/// Genesis hash and optional classification of a public Solana cluster.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct GenesisHashResult {
/// Base58 genesis hash returned by the endpoint.
pub genesis_hash: std::string::String,
/// Known public cluster when the hash matches an official public cluster.
pub classified_cluster: std::option::Option<ks_lib::ExApiExecutionCluster>,
}
/// Configuration for `getEpochInfo` used by stateful execution readiness checks.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetEpochInfoConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::GetEpochInfoConfig {
/// Creates an epoch information request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { commitment, min_context_slot };
}
/// Creates the default confirmed epoch information request.
pub fn confirmed() -> Self {
return Self::new(crate::RpcCommitmentLevel::Confirmed, std::option::Option::None);
}
fn request_params(&self) -> std::vec::Vec<serde_json::Value> {
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return vec![serde_json::Value::Object(config)];
}
}
/// Current epoch and slot progression returned by `getEpochInfo`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EpochInfoResult {
/// Absolute cluster slot.
pub absolute_slot: u64,
/// Current block height.
pub block_height: u64,
/// Current epoch number.
pub epoch: u64,
/// Slot index within the current epoch.
pub slot_index: u64,
/// Number of slots in the current epoch.
pub slots_in_epoch: u64,
/// Optional transaction count when the node exposes it.
pub transaction_count: std::option::Option<u64>,
}
/// Configuration for `getLatestBlockhash`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetLatestBlockhashConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::GetLatestBlockhashConfig {
/// Creates a blockhash request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { commitment, min_context_slot };
}
/// Creates the default confirmed blockhash request.
pub fn confirmed() -> Self {
return Self::new(crate::RpcCommitmentLevel::Confirmed, std::option::Option::None);
}
fn request_params(&self) -> std::vec::Vec<serde_json::Value> {
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::vec![serde_json::Value::Object(config)];
}
}
/// Latest recent blockhash returned by the cluster.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct LatestBlockhashResult {
/// RPC response context.
pub context: crate::RpcResponseContext,
/// Base58 recent blockhash.
pub blockhash: std::string::String,
/// Last block height at which the blockhash remains valid.
pub last_valid_block_height: u64,
}
/// Configuration for `getFeeForMessage`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetFeeForMessageConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::GetFeeForMessageConfig {
/// Creates a fee request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { commitment, min_context_slot };
}
/// Creates the default confirmed fee request.
pub fn confirmed() -> Self {
return Self::new(crate::RpcCommitmentLevel::Confirmed, std::option::Option::None);
}
fn request_params(
&self,
encoded_message: &str,
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
validate_base64_payload(encoded_message, "getFeeForMessage message");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(encoded_message.to_string()),
serde_json::Value::Object(config),
]);
}
}
/// Fee estimate returned for one serialized transaction message.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct FeeForMessageResult {
/// RPC response context.
pub context: crate::RpcResponseContext,
/// Fee in lamports, or `None` when the referenced blockhash is no longer valid.
pub fee_lamports: std::option::Option<u64>,
}
/// Optional accounts requested from `simulateTransaction`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimulationAccountsConfig {
/// Account addresses returned after simulation.
pub addresses: std::vec::Vec<ks_lib::MdPubkey>,
}
impl crate::SimulationAccountsConfig {
/// Creates a bounded account-return request using base64 account data.
pub fn new(addresses: std::vec::Vec<ks_lib::MdPubkey>) -> ks_core::Result<Self> {
if addresses.is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"simulateTransaction accounts must not be empty",
));
}
if addresses.len() > crate::MAX_SIMULATION_ACCOUNT_COUNT {
return std::result::Result::Err(ks_core::Error::config(format!(
"simulateTransaction accounts must not exceed {} entries",
crate::MAX_SIMULATION_ACCOUNT_COUNT
)));
}
for address in &addresses {
let validation_result = crate::validate_solana_pubkey_text(
address.0.as_str(),
"simulateTransaction account address",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(Self { addresses });
}
fn request_value(&self) -> serde_json::Value {
let addresses = self
.addresses
.iter()
.map(|address| return serde_json::Value::String(address.0.clone()))
.collect::<std::vec::Vec<_>>();
return serde_json::json!({
"encoding": "base64",
"addresses": addresses
});
}
}
/// Configuration for `simulateTransaction`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimulateTransactionConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Whether transaction signatures must be verified by the node.
pub signature_verification: bool,
/// Whether the node should replace the recent blockhash before simulation.
pub replace_recent_blockhash: bool,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
/// Whether inner instructions should be returned.
pub inner_instructions: bool,
/// Optional post-simulation account values to return.
pub accounts: std::option::Option<crate::SimulationAccountsConfig>,
}
impl crate::SimulateTransactionConfig {
/// Creates and validates a simulation request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
signature_verification: bool,
replace_recent_blockhash: bool,
min_context_slot: std::option::Option<u64>,
inner_instructions: bool,
accounts: std::option::Option<crate::SimulationAccountsConfig>,
) -> ks_core::Result<Self> {
if signature_verification && replace_recent_blockhash {
return std::result::Result::Err(ks_core::Error::config(
"simulateTransaction cannot verify signatures while replacing the recent blockhash",
));
}
return std::result::Result::Ok(Self {
commitment,
signature_verification,
replace_recent_blockhash,
min_context_slot,
inner_instructions,
accounts,
});
}
/// Creates an unsigned simulation configuration for the exact supplied blockhash.
pub fn unsigned_exact() -> Self {
return Self {
commitment: crate::RpcCommitmentLevel::Confirmed,
signature_verification: false,
replace_recent_blockhash: false,
min_context_slot: std::option::Option::None,
inner_instructions: true,
accounts: std::option::Option::None,
};
}
/// Creates an unsigned diagnostic simulation that replaces the recent blockhash.
///
/// Its result cannot authorize signing of the original message.
pub fn unsigned_with_replacement() -> Self {
return Self {
commitment: crate::RpcCommitmentLevel::Confirmed,
signature_verification: false,
replace_recent_blockhash: true,
min_context_slot: std::option::Option::None,
inner_instructions: true,
accounts: std::option::Option::None,
};
}
fn request_params(
&self,
encoded_transaction: &str,
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
validate_base64_payload(encoded_transaction, "simulateTransaction transaction");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
config.insert("encoding".to_string(), serde_json::Value::String("base64".to_string()));
config
.insert("sigVerify".to_string(), serde_json::Value::Bool(self.signature_verification));
config.insert(
"replaceRecentBlockhash".to_string(),
serde_json::Value::Bool(self.replace_recent_blockhash),
);
config.insert(
"innerInstructions".to_string(),
serde_json::Value::Bool(self.inner_instructions),
);
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
if let std::option::Option::Some(accounts) = &self.accounts {
config.insert("accounts".to_string(), accounts.request_value());
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(encoded_transaction.to_string()),
serde_json::Value::Object(config),
]);
}
}
/// Replacement blockhash returned by an unsigned simulation.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimulationReplacementBlockhash {
/// Base58 replacement blockhash.
pub blockhash: std::string::String,
/// Last valid block height for the replacement blockhash.
pub last_valid_block_height: u64,
}
/// Typed result returned by `simulateTransaction`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SimulateTransactionResult {
/// RPC response context.
pub context: crate::RpcResponseContext,
/// Whether the transaction completed without a runtime error.
pub success: bool,
/// Runtime error value returned by the node.
pub error: std::option::Option<serde_json::Value>,
/// Runtime logs in execution order.
pub logs: std::vec::Vec<std::string::String>,
/// Compute units consumed when reported.
pub units_consumed: std::option::Option<u64>,
/// Fee reported by simulation when supported by the node.
pub fee_lamports: std::option::Option<u64>,
/// Total loaded account data size when reported.
pub loaded_accounts_data_size: std::option::Option<u64>,
/// Replacement recent blockhash when requested.
pub replacement_blockhash: std::option::Option<crate::SimulationReplacementBlockhash>,
/// Program return data retained as provider-neutral JSON.
pub return_data: std::option::Option<serde_json::Value>,
/// Inner instructions retained as provider-neutral JSON.
pub inner_instructions: std::option::Option<serde_json::Value>,
/// Requested account snapshots retained as provider-neutral JSON.
pub accounts: std::option::Option<std::vec::Vec<std::option::Option<serde_json::Value>>>,
}
impl crate::SimulateTransactionResult {
/// Converts this RPC result into the common execution simulation contract.
pub fn to_execution_result(
&self,
cluster: ks_lib::ExApiExecutionCluster,
blockhash_kind: ks_lib::ExApiExecutionBlockhashKind,
blockhash_age_slots: std::option::Option<u64>,
nonce_account: std::option::Option<ks_lib::MdPubkey>,
nonce_authority: std::option::Option<ks_lib::MdPubkey>,
fee_fallback: std::option::Option<&crate::FeeForMessageResult>,
) -> ks_lib::ExApiExecutionSimulationResult {
let estimated_fee_lamports = match self.fee_lamports {
std::option::Option::Some(fee) => std::option::Option::Some(fee),
std::option::Option::None => fee_fallback.and_then(|result| return result.fee_lamports),
};
let error = self.error.as_ref().map(|value| return value.to_string());
let replacement_blockhash = self
.replacement_blockhash
.as_ref()
.map(|replacement| return replacement.blockhash.clone());
let replacement_last_valid_block_height = self
.replacement_blockhash
.as_ref()
.map(|replacement| return replacement.last_valid_block_height);
return ks_lib::ExApiExecutionSimulationResult {
simulated: true,
success: self.success,
cluster,
blockhash_kind,
blockhash_age_slots,
replacement_blockhash,
replacement_last_valid_block_height,
nonce_account,
nonce_authority,
units_consumed: self.units_consumed,
estimated_fee_lamports,
logs: self.logs.clone(),
return_data: self.return_data.clone(),
error,
};
}
}
/// Configuration for `getBalance`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBalanceConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::GetBalanceConfig {
/// Creates a balance request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { commitment, min_context_slot };
}
/// Creates the default confirmed balance request.
pub fn confirmed() -> Self {
return Self::new(crate::RpcCommitmentLevel::Confirmed, std::option::Option::None);
}
fn request_params(
&self,
pubkey: &ks_lib::MdPubkey,
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_solana_pubkey_text(pubkey.0.as_str(), "getBalance account");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(pubkey.0.clone()),
serde_json::Value::Object(config),
]);
}
}
/// Lamport balance returned for one account.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct BalanceResult {
/// RPC response context.
pub context: crate::RpcResponseContext,
/// Account balance in lamports.
pub lamports: u64,
}
/// Configuration for `getAccountInfo` used by execution readiness checks.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetAccountInfoConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
/// Maximum complete decoded account-data length, or `None` for metadata-only requests.
pub max_data_bytes: std::option::Option<usize>,
}
impl crate::GetAccountInfoConfig {
/// Creates an account information request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self {
commitment,
min_context_slot,
max_data_bytes: std::option::Option::None,
};
}
/// Creates a complete account-data request bounded by the decoded byte limit.
pub fn new_with_data(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
max_data_bytes: usize,
) -> ks_core::Result<Self> {
if max_data_bytes == 0 || max_data_bytes > crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES {
return std::result::Result::Err(ks_core::Error::config(format!(
"getAccountInfo complete data limit must be between 1 and {} bytes",
crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES
)));
}
return std::result::Result::Ok(Self {
commitment,
min_context_slot,
max_data_bytes: std::option::Option::Some(max_data_bytes),
});
}
/// Creates the default confirmed metadata-only account information request.
pub fn confirmed() -> Self {
return Self::new(crate::RpcCommitmentLevel::Confirmed, std::option::Option::None);
}
/// Creates a confirmed complete account-data request bounded by the decoded byte limit.
pub fn confirmed_with_data(max_data_bytes: usize) -> ks_core::Result<Self> {
return Self::new_with_data(
crate::RpcCommitmentLevel::Confirmed,
std::option::Option::None,
max_data_bytes,
);
}
fn request_params(
&self,
pubkey: &ks_lib::MdPubkey,
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
if let std::option::Option::Some(max_data_bytes) = self.max_data_bytes {
if max_data_bytes == 0 || max_data_bytes > crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES {
return std::result::Result::Err(ks_core::Error::config(format!(
"getAccountInfo complete data limit must be between 1 and {} bytes",
crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES
)));
}
}
let validation_result =
crate::validate_solana_pubkey_text(pubkey.0.as_str(), "getAccountInfo account");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
config.insert("encoding".to_string(), serde_json::Value::String("base64".to_string()));
if self.max_data_bytes.is_none() {
config.insert("dataSlice".to_string(), serde_json::json!({"offset": 0, "length": 0}));
}
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(pubkey.0.clone()),
serde_json::Value::Object(config),
]);
}
}
/// Bounded account metadata and optional complete decoded data returned by `getAccountInfo`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct AccountInfoValue {
/// Account balance in lamports.
pub lamports: u64,
/// Program that owns the account.
pub owner: ks_lib::MdProgramId,
/// Whether the account contains executable program code.
pub executable: bool,
/// Rent epoch reported by the node.
pub rent_epoch: u64,
/// Complete on-chain account-data length reported independently by the node.
pub space: u64,
/// Decoded account bytes, empty for metadata-only requests.
pub data: std::vec::Vec<u8>,
}
/// Contextual result returned by `getAccountInfo`.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct AccountInfoResult {
/// RPC response context.
pub context: crate::RpcResponseContext,
/// Account value, or `None` when the address does not exist.
pub account: std::option::Option<crate::AccountInfoValue>,
}
/// Configuration for `getMinimumBalanceForRentExemption`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetMinimumBalanceForRentExemptionConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
}
impl crate::GetMinimumBalanceForRentExemptionConfig {
/// Creates a rent-exemption request configuration.
pub fn new(commitment: crate::RpcCommitmentLevel) -> Self {
return Self { commitment };
}
/// Creates the default confirmed rent-exemption request.
pub fn confirmed() -> Self {
return Self::new(crate::RpcCommitmentLevel::Confirmed);
}
fn request_params(&self, data_length: u64) -> std::vec::Vec<serde_json::Value> {
return std::vec![
serde_json::Value::Number(serde_json::Number::from(data_length)),
serde_json::json!({"commitment": self.commitment.as_str()}),
];
}
}
/// Rent-exempt minimum returned for one account data length.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MinimumBalanceForRentExemptionResult {
/// Account data length used by the request.
pub data_length: u64,
/// Minimum balance in lamports.
pub minimum_balance_lamports: u64,
}
/// Configuration for `requestAirdrop`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RequestAirdropConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional recent blockhash supplied to the faucet.
pub recent_blockhash: std::option::Option<std::string::String>,
}
impl crate::RequestAirdropConfig {
/// Creates an airdrop request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
recent_blockhash: std::option::Option<std::string::String>,
) -> ks_core::Result<Self> {
if let std::option::Option::Some(blockhash) = &recent_blockhash {
let validation_result = crate::validate_solana_hash_text(
blockhash.as_str(),
"requestAirdrop recent blockhash",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(Self { commitment, recent_blockhash });
}
/// Creates the default confirmed airdrop request.
pub fn confirmed() -> Self {
return Self {
commitment: crate::RpcCommitmentLevel::Confirmed,
recent_blockhash: std::option::Option::None,
};
}
fn request_params(
&self,
pubkey: &ks_lib::MdPubkey,
lamports: u64,
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
crate::validate_solana_pubkey_text(pubkey.0.as_str(), "requestAirdrop recipient");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
if lamports == 0 {
return std::result::Result::Err(ks_core::Error::config(
"requestAirdrop lamports must be greater than zero",
));
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
if let std::option::Option::Some(blockhash) = &self.recent_blockhash {
config.insert(
"recentBlockhash".to_string(),
serde_json::Value::String(blockhash.clone()),
);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(pubkey.0.clone()),
serde_json::Value::Number(serde_json::Number::from(lamports)),
serde_json::Value::Object(config),
]);
}
}
/// Signature returned by a faucet airdrop request.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct AirdropResult {
/// Faucet transaction signature.
pub signature: ks_lib::MdSignature,
}
/// Configuration for `sendTransaction`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SendTransactionConfig {
/// Whether the node should skip signature verification and preflight simulation.
pub skip_preflight: bool,
/// Commitment used by the node's preflight simulation.
pub preflight_commitment: crate::RpcCommitmentLevel,
/// Optional maximum number of node retransmission retries.
pub max_retries: std::option::Option<u32>,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::SendTransactionConfig {
/// Creates a validated transaction submission configuration.
pub fn new(
skip_preflight: bool,
preflight_commitment: crate::RpcCommitmentLevel,
max_retries: std::option::Option<u32>,
min_context_slot: std::option::Option<u64>,
) -> ks_core::Result<Self> {
if skip_preflight {
return std::result::Result::Err(ks_core::Error::config(
"sendTransaction preflight cannot be skipped by execution orchestration",
));
}
return std::result::Result::Ok(Self {
skip_preflight,
preflight_commitment,
max_retries,
min_context_slot,
});
}
/// Creates the default confirmed submission configuration with three retries.
pub fn confirmed() -> Self {
return Self {
skip_preflight: false,
preflight_commitment: crate::RpcCommitmentLevel::Confirmed,
max_retries: std::option::Option::Some(3),
min_context_slot: std::option::Option::None,
};
}
/// Creates a confirmed submission configuration from the active execution settings.
pub fn from_execution_config(
config: &ks_config::ExecutionConfig,
min_context_slot: std::option::Option<u64>,
) -> ks_core::Result<Self> {
return Self::new(
false,
crate::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(config.send_max_retries),
min_context_slot,
);
}
fn request_params(
&self,
encoded_transaction: &str,
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
let validation_result =
validate_base64_payload(encoded_transaction, "sendTransaction signed transaction");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert("encoding".to_string(), serde_json::Value::String("base64".to_string()));
config.insert("skipPreflight".to_string(), serde_json::Value::Bool(self.skip_preflight));
config.insert(
"preflightCommitment".to_string(),
serde_json::Value::String(self.preflight_commitment.as_str().to_string()),
);
if let std::option::Option::Some(max_retries) = self.max_retries {
config.insert(
"maxRetries".to_string(),
serde_json::Value::Number(serde_json::Number::from(max_retries)),
);
}
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::result::Result::Ok(std::vec![
serde_json::Value::String(encoded_transaction.to_string()),
serde_json::Value::Object(config),
]);
}
}
/// Result returned after a node accepts a signed transaction for relay.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SendTransactionResult {
/// First signature embedded in the submitted transaction.
pub signature: ks_lib::MdSignature,
}
impl crate::SendTransactionResult {
/// Converts the RPC result into the common execution send contract.
pub fn to_execution_result(
&self,
cluster: ks_lib::ExApiExecutionCluster,
) -> ks_lib::ExApiExecutionSendResult {
return ks_lib::ExApiExecutionSendResult {
cluster,
signature: self.signature.clone(),
submitted: true,
};
}
}
/// Configuration for `getSignatureStatuses`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetSignatureStatusesConfig {
/// Whether the node may search its full transaction history.
pub search_transaction_history: bool,
}
impl crate::GetSignatureStatusesConfig {
/// Creates a status request configuration.
pub fn new(search_transaction_history: bool) -> Self {
return Self { search_transaction_history };
}
fn request_params(
&self,
signatures: &[ks_lib::MdSignature],
) -> ks_core::Result<std::vec::Vec<serde_json::Value>> {
if signatures.is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"getSignatureStatuses requires at least one signature",
));
}
if signatures.len() > crate::MAX_SIGNATURE_STATUS_COUNT {
return std::result::Result::Err(ks_core::Error::config(format!(
"getSignatureStatuses supports at most {} signatures",
crate::MAX_SIGNATURE_STATUS_COUNT
)));
}
let mut values = std::vec::Vec::with_capacity(signatures.len());
for signature in signatures {
let validation_result = crate::validate_transaction_signature_text(
signature.0.as_str(),
"getSignatureStatuses signature",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
values.push(serde_json::Value::String(signature.0.clone()));
}
return std::result::Result::Ok(std::vec![
serde_json::Value::Array(values),
serde_json::json!({
"searchTransactionHistory": self.search_transaction_history
}),
]);
}
}
/// Current status for one submitted transaction signature.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SignatureStatus {
/// Slot that processed the transaction.
pub slot: u64,
/// Number of blocks since processing, or `None` once rooted/finalized.
pub confirmations: std::option::Option<u64>,
/// Runtime transaction error when execution failed.
pub error: std::option::Option<serde_json::Value>,
/// Highest commitment reported by the node.
pub confirmation_status: std::option::Option<crate::RpcCommitmentLevel>,
}
/// Status response preserving positional correspondence with requested signatures.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct SignatureStatusesResult {
/// RPC response context.
pub context: crate::RpcResponseContext,
/// Status values in request order; absent entries have not been observed.
pub statuses: std::vec::Vec<std::option::Option<crate::SignatureStatus>>,
}
/// Configuration for `getBlockHeight`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GetBlockHeightConfig {
/// Requested commitment.
pub commitment: crate::RpcCommitmentLevel,
/// Optional minimum context slot.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::GetBlockHeightConfig {
/// Creates a block-height request configuration.
pub fn new(
commitment: crate::RpcCommitmentLevel,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { commitment, min_context_slot };
}
fn request_params(&self) -> std::vec::Vec<serde_json::Value> {
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
config.insert(
"commitment".to_string(),
serde_json::Value::String(self.commitment.as_str().to_string()),
);
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
config.insert(
"minContextSlot".to_string(),
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
);
}
return std::vec![serde_json::Value::Object(config)];
}
}
/// Current block height returned by the node.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct BlockHeightResult {
/// Block height observed at the requested commitment.
pub block_height: u64,
}
/// Bounded polling policy used to confirm one submitted transaction.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfirmTransactionConfig {
/// Commitment required before confirmation succeeds.
pub commitment: crate::RpcCommitmentLevel,
/// Whether status polling may search transaction history.
pub search_transaction_history: bool,
/// Delay between status polls.
pub poll_interval_ms: u64,
/// Maximum number of status polls.
pub max_attempts: u32,
/// Last valid block height of the recent blockhash, when applicable.
pub last_valid_block_height: std::option::Option<u64>,
/// Optional minimum context slot for block-height checks.
pub min_context_slot: std::option::Option<u64>,
}
impl crate::ConfirmTransactionConfig {
/// Creates a bounded confirmation policy.
pub fn new(
commitment: crate::RpcCommitmentLevel,
search_transaction_history: bool,
poll_interval_ms: u64,
max_attempts: u32,
last_valid_block_height: std::option::Option<u64>,
min_context_slot: std::option::Option<u64>,
) -> ks_core::Result<Self> {
if poll_interval_ms == 0 || poll_interval_ms > crate::MAX_CONFIRMATION_POLL_INTERVAL_MS {
return std::result::Result::Err(ks_core::Error::config(format!(
"confirmation poll interval must be between 1 and {} milliseconds",
crate::MAX_CONFIRMATION_POLL_INTERVAL_MS
)));
}
if max_attempts == 0 || max_attempts > crate::MAX_CONFIRMATION_ATTEMPTS {
return std::result::Result::Err(ks_core::Error::config(format!(
"confirmation attempts must be between 1 and {}",
crate::MAX_CONFIRMATION_ATTEMPTS
)));
}
return std::result::Result::Ok(Self {
commitment,
search_transaction_history,
poll_interval_ms,
max_attempts,
last_valid_block_height,
min_context_slot,
});
}
/// Creates a confirmed recent-blockhash confirmation policy.
pub fn confirmed(
last_valid_block_height: u64,
poll_interval_ms: u64,
max_attempts: u32,
) -> ks_core::Result<Self> {
return Self::new(
crate::RpcCommitmentLevel::Confirmed,
false,
poll_interval_ms,
max_attempts,
std::option::Option::Some(last_valid_block_height),
std::option::Option::None,
);
}
/// Creates a confirmed bounded policy from the active execution settings.
pub fn from_execution_config(
config: &ks_config::ExecutionConfig,
last_valid_block_height: std::option::Option<u64>,
min_context_slot: std::option::Option<u64>,
) -> ks_core::Result<Self> {
return Self::new(
crate::RpcCommitmentLevel::Confirmed,
false,
config.confirmation_poll_interval_ms,
config.confirmation_max_attempts,
last_valid_block_height,
min_context_slot,
);
}
}
/// Classifies a genesis hash as one of the three official public clusters.
pub fn classify_genesis_hash(
genesis_hash: &str,
) -> std::option::Option<ks_lib::ExApiExecutionCluster> {
return match genesis_hash {
crate::DEVNET_GENESIS_HASH => {
std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet)
},
crate::TESTNET_GENESIS_HASH => {
std::option::Option::Some(ks_lib::ExApiExecutionCluster::Testnet)
},
crate::MAINNET_GENESIS_HASH => {
std::option::Option::Some(ks_lib::ExApiExecutionCluster::Mainnet)
},
_ => std::option::Option::None,
};
}
/// Adapts a raw `getGenesisHash` result.
pub fn adapt_get_genesis_hash_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::GenesisHashResult> {
let genesis_hash = match source.as_str() {
std::option::Option::Some(value) => value.to_string(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getGenesisHash result must be a string",
));
},
};
let validation_result =
crate::validate_solana_hash_text(genesis_hash.as_str(), "getGenesisHash result");
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::GenesisHashResult {
classified_cluster: crate::classify_genesis_hash(genesis_hash.as_str()),
genesis_hash,
});
}
/// Adapts a raw `getLatestBlockhash` result.
pub fn adapt_get_latest_blockhash_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::LatestBlockhashResult> {
let parse_result =
serde_json::from_value::<RpcContextValue<RpcLatestBlockhashValue>>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse getLatestBlockhash result: {error}"
)));
},
};
let validation_result = crate::validate_solana_hash_text(
parsed.value.blockhash.as_str(),
"getLatestBlockhash blockhash",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::LatestBlockhashResult {
context: parsed.context,
blockhash: parsed.value.blockhash,
last_valid_block_height: parsed.value.last_valid_block_height,
});
}
/// Adapts a raw `getFeeForMessage` result.
pub fn adapt_get_fee_for_message_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::FeeForMessageResult> {
let parse_result =
serde_json::from_value::<RpcContextValue<std::option::Option<u64>>>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse getFeeForMessage result: {error}"
)));
},
};
return std::result::Result::Ok(crate::FeeForMessageResult {
context: parsed.context,
fee_lamports: parsed.value,
});
}
/// Adapts a raw `simulateTransaction` result.
pub fn adapt_simulate_transaction_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::SimulateTransactionResult> {
let parse_result =
serde_json::from_value::<RpcContextValue<RpcSimulationValue>>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse simulateTransaction result: {error}"
)));
},
};
if let std::option::Option::Some(replacement) = &parsed.value.replacement_blockhash {
let validation_result = crate::validate_solana_hash_text(
replacement.blockhash.as_str(),
"simulateTransaction replacement blockhash",
);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
}
let logs = match parsed.value.logs {
std::option::Option::Some(logs) => logs,
std::option::Option::None => std::vec::Vec::new(),
};
return std::result::Result::Ok(crate::SimulateTransactionResult {
context: parsed.context,
success: parsed.value.error.is_none(),
error: parsed.value.error,
logs,
units_consumed: parsed.value.units_consumed,
fee_lamports: parsed.value.fee,
loaded_accounts_data_size: parsed.value.loaded_accounts_data_size,
replacement_blockhash: parsed.value.replacement_blockhash,
return_data: parsed.value.return_data,
inner_instructions: parsed.value.inner_instructions,
accounts: parsed.value.accounts,
});
}
/// Adapts a raw `getBalance` result.
pub fn adapt_get_balance_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::BalanceResult> {
let parse_result = serde_json::from_value::<RpcContextValue<u64>>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse getBalance result: {error}"
)));
},
};
return std::result::Result::Ok(crate::BalanceResult {
context: parsed.context,
lamports: parsed.value,
});
}
/// Adapts a raw metadata-only `getAccountInfo` result.
pub fn adapt_get_account_info_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::AccountInfoResult> {
return crate::adapt_get_account_info_result_with_data_limit(source, std::option::Option::None);
}
pub(crate) fn adapt_get_account_info_result_with_data_limit(
source: &serde_json::Value,
max_data_bytes: std::option::Option<usize>,
) -> ks_core::Result<crate::AccountInfoResult> {
let context = match source.get("context") {
std::option::Option::Some(value) => {
let parse_result = serde_json::from_value::<crate::RpcResponseContext>(value.clone());
match parse_result {
std::result::Result::Ok(context) => context,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse getAccountInfo context: {error}"
)));
},
}
},
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo result is missing context",
));
},
};
let value = match source.get("value") {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo result is missing value",
));
},
};
if value.is_null() {
return std::result::Result::Ok(crate::AccountInfoResult {
context,
account: std::option::Option::None,
});
}
let lamports = match value.get("lamports").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account lamports must be an unsigned integer",
));
},
};
let owner_text = match value.get("owner").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account owner must be a string",
));
},
};
let owner_validation =
crate::validate_solana_pubkey_text(owner_text, "getAccountInfo account owner");
if let std::result::Result::Err(error) = owner_validation {
return std::result::Result::Err(error);
}
let executable = match value.get("executable").and_then(serde_json::Value::as_bool) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account executable must be a boolean",
));
},
};
let rent_epoch = match value.get("rentEpoch").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account rentEpoch must be an unsigned integer",
));
},
};
let space = match value.get("space").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account space must be an unsigned integer",
));
},
};
let data = match decode_account_data(value, space, max_data_bytes) {
std::result::Result::Ok(data) => data,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::AccountInfoResult {
context,
account: std::option::Option::Some(crate::AccountInfoValue {
lamports,
owner: ks_lib::MdProgramId(owner_text.to_string()),
executable,
rent_epoch,
space,
data,
}),
});
}
fn decode_account_data(
value: &serde_json::Value,
space: u64,
max_data_bytes: std::option::Option<usize>,
) -> ks_core::Result<std::vec::Vec<u8>> {
let tuple = match value.get("data").and_then(serde_json::Value::as_array) {
std::option::Option::Some(tuple) if tuple.len() == 2 => tuple,
_ => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account data must be a two-element encoded tuple",
));
},
};
let encoded = match tuple.first().and_then(serde_json::Value::as_str) {
std::option::Option::Some(encoded) => encoded,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account data payload must be a string",
));
},
};
let encoding = match tuple.get(1).and_then(serde_json::Value::as_str) {
std::option::Option::Some(encoding) => encoding,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getAccountInfo account data encoding must be a string",
));
},
};
if encoding != "base64" {
return std::result::Result::Err(ks_core::Error::json(format!(
"getAccountInfo account data encoding {encoding} is unsupported"
)));
}
match max_data_bytes {
std::option::Option::None => {
if !encoded.is_empty() {
return std::result::Result::Err(ks_core::Error::json(
"metadata-only getAccountInfo returned non-empty account data",
));
}
},
std::option::Option::Some(max_data_bytes) => {
if max_data_bytes == 0 || max_data_bytes > crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES {
return std::result::Result::Err(ks_core::Error::json(format!(
"getAccountInfo complete data limit must be between 1 and {} bytes",
crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES
)));
}
if space > max_data_bytes as u64 {
return std::result::Result::Err(ks_core::Error::json(format!(
"getAccountInfo account space {space} exceeds the requested complete-data limit {max_data_bytes}"
)));
}
let maximum_encoded_length = max_data_bytes.div_ceil(3) * 4;
if encoded.len() > maximum_encoded_length {
return std::result::Result::Err(ks_core::Error::json(format!(
"getAccountInfo encoded account data length {} exceeds the limit {maximum_encoded_length}",
encoded.len()
)));
}
},
}
let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded) {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot decode getAccountInfo base64 account data: {error}"
)));
},
};
if let std::option::Option::Some(max_data_bytes) = max_data_bytes {
if decoded.len() > max_data_bytes {
return std::result::Result::Err(ks_core::Error::json(format!(
"getAccountInfo decoded account data length {} exceeds the requested limit {max_data_bytes}",
decoded.len()
)));
}
if decoded.len() as u64 != space {
return std::result::Result::Err(ks_core::Error::json(format!(
"getAccountInfo returned {} decoded bytes for an account reporting {space} bytes",
decoded.len()
)));
}
}
return std::result::Result::Ok(decoded);
}
/// Adapts a raw `getMinimumBalanceForRentExemption` result.
pub fn adapt_get_minimum_balance_for_rent_exemption_result(
source: &serde_json::Value,
data_length: u64,
) -> ks_core::Result<crate::MinimumBalanceForRentExemptionResult> {
let minimum_balance_lamports = match source.as_u64() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getMinimumBalanceForRentExemption result must be an unsigned integer",
));
},
};
return std::result::Result::Ok(crate::MinimumBalanceForRentExemptionResult {
data_length,
minimum_balance_lamports,
});
}
/// Adapts a raw `requestAirdrop` result.
pub fn adapt_request_airdrop_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::AirdropResult> {
let signature = match adapt_signature_result(source, "requestAirdrop result") {
std::result::Result::Ok(signature) => signature,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::AirdropResult { signature });
}
/// Adapts a raw `sendTransaction` result and verifies the returned primary signature.
pub fn adapt_send_transaction_result(
source: &serde_json::Value,
expected_signature: &ks_lib::MdSignature,
) -> ks_core::Result<crate::SendTransactionResult> {
let expected_validation = crate::validate_transaction_signature_text(
expected_signature.0.as_str(),
"sendTransaction expected signature",
);
if let std::result::Result::Err(error) = expected_validation {
return std::result::Result::Err(error);
}
let signature = match adapt_signature_result(source, "sendTransaction result") {
std::result::Result::Ok(signature) => signature,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if signature != *expected_signature {
return std::result::Result::Err(ks_core::Error::new(
"execution_send_signature_mismatch",
format!(
"sendTransaction returned signature '{}' but the signed transaction primary signature is '{}'",
signature.0, expected_signature.0
),
));
}
return std::result::Result::Ok(crate::SendTransactionResult { signature });
}
/// Adapts a raw `getSignatureStatuses` result.
pub fn adapt_get_signature_statuses_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::SignatureStatusesResult> {
let parse_result = serde_json::from_value::<
RpcContextValue<std::vec::Vec<std::option::Option<RpcSignatureStatusValue>>>,
>(source.clone());
let parsed = match parse_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse getSignatureStatuses result: {error}"
)));
},
};
let mut statuses = std::vec::Vec::with_capacity(parsed.value.len());
for value in parsed.value {
let status = value.map(|value| {
return crate::SignatureStatus {
slot: value.slot,
confirmations: value.confirmations,
error: value.error,
confirmation_status: value.confirmation_status,
};
});
statuses.push(status);
}
return std::result::Result::Ok(crate::SignatureStatusesResult {
context: parsed.context,
statuses,
});
}
/// Adapts a raw `getEpochInfo` result.
pub fn adapt_get_epoch_info_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::EpochInfoResult> {
let parse_result = serde_json::from_value::<crate::EpochInfoResult>(source.clone());
return match parse_result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::json(format!(
"cannot parse getEpochInfo result: {error}"
))),
};
}
/// Adapts a raw `getBlockHeight` result.
pub fn adapt_get_block_height_result(
source: &serde_json::Value,
) -> ks_core::Result<crate::BlockHeightResult> {
let block_height = match source.as_u64() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(
"getBlockHeight result must be an unsigned integer",
));
},
};
return std::result::Result::Ok(crate::BlockHeightResult { block_height });
}
impl crate::HttpClient {
/// Returns the connected cluster genesis hash and known public-cluster classification.
pub async fn get_genesis_hash(&self) -> ks_core::Result<crate::GenesisHashResult> {
let result = self
.execute_json_rpc_result_raw("getGenesisHash".to_string(), std::vec::Vec::new())
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_genesis_hash_result(&value);
}
/// Returns current epoch and slot progression information.
pub async fn get_epoch_info(
&self,
config: &crate::GetEpochInfoConfig,
) -> ks_core::Result<crate::EpochInfoResult> {
let result = self
.execute_json_rpc_result_raw("getEpochInfo".to_string(), config.request_params())
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_epoch_info_result(&value);
}
/// Returns the latest blockhash and its last valid block height.
pub async fn get_latest_blockhash(
&self,
config: &crate::GetLatestBlockhashConfig,
) -> ks_core::Result<crate::LatestBlockhashResult> {
let result = self
.execute_json_rpc_result_raw("getLatestBlockhash".to_string(), config.request_params())
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_latest_blockhash_result(&value);
}
/// Returns the fee the cluster would charge for a serialized message.
pub async fn get_fee_for_message(
&self,
encoded_message: &str,
config: &crate::GetFeeForMessageConfig,
) -> ks_core::Result<crate::FeeForMessageResult> {
let params_result = config.request_params(encoded_message);
let params = match params_result {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self.execute_json_rpc_result_raw("getFeeForMessage".to_string(), params).await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_fee_for_message_result(&value);
}
/// Simulates one base64-encoded transaction without broadcasting it.
pub async fn simulate_transaction(
&self,
encoded_transaction: &str,
config: &crate::SimulateTransactionConfig,
) -> ks_core::Result<crate::SimulateTransactionResult> {
let params_result = config.request_params(encoded_transaction);
let params = match params_result {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self
.execute_json_rpc_result_raw("simulateTransaction".to_string(), params)
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_simulate_transaction_result(&value);
}
/// Returns the lamport balance for one account.
pub async fn get_balance(
&self,
pubkey: &ks_lib::MdPubkey,
config: &crate::GetBalanceConfig,
) -> ks_core::Result<crate::BalanceResult> {
let params = match config.request_params(pubkey) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self.execute_json_rpc_result_raw("getBalance".to_string(), params).await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_balance_result(&value);
}
/// Returns minimal account metadata or `None` when the address does not exist.
pub async fn get_account_info(
&self,
pubkey: &ks_lib::MdPubkey,
config: &crate::GetAccountInfoConfig,
) -> ks_core::Result<crate::AccountInfoResult> {
let params = match config.request_params(pubkey) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self.execute_json_rpc_result_raw("getAccountInfo".to_string(), params).await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_account_info_result_with_data_limit(&value, config.max_data_bytes);
}
/// Returns the rent-exempt minimum for one account data length.
pub async fn get_minimum_balance_for_rent_exemption(
&self,
data_length: u64,
config: &crate::GetMinimumBalanceForRentExemptionConfig,
) -> ks_core::Result<crate::MinimumBalanceForRentExemptionResult> {
let result = self
.execute_json_rpc_result_raw(
"getMinimumBalanceForRentExemption".to_string(),
config.request_params(data_length),
)
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_minimum_balance_for_rent_exemption_result(&value, data_length);
}
/// Requests a faucet airdrop to one account.
pub async fn request_airdrop(
&self,
pubkey: &ks_lib::MdPubkey,
lamports: u64,
config: &crate::RequestAirdropConfig,
) -> ks_core::Result<crate::AirdropResult> {
let params = match config.request_params(pubkey, lamports) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self.execute_json_rpc_result_raw("requestAirdrop".to_string(), params).await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_request_airdrop_result(&value);
}
/// Submits one fully signed base64 transaction and verifies the returned signature.
pub async fn send_transaction(
&self,
encoded_transaction: &str,
expected_signature: &ks_lib::MdSignature,
config: &crate::SendTransactionConfig,
) -> ks_core::Result<crate::SendTransactionResult> {
let params = match config.request_params(encoded_transaction) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self.execute_json_rpc_result_raw("sendTransaction".to_string(), params).await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let adapted = match crate::adapt_send_transaction_result(&value, expected_signature) {
std::result::Result::Ok(adapted) => adapted,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
tracing::info!(
target: crate::TRACING_TARGET,
action = "send_transaction",
endpoint_name = %self.endpoint_name(),
provider = %self.provider(),
signature = %adapted.signature.0,
status = "submitted",
"signed transaction accepted for relay"
);
return std::result::Result::Ok(adapted);
}
/// Returns current statuses for a bounded signature list.
pub async fn get_signature_statuses(
&self,
signatures: &[ks_lib::MdSignature],
config: &crate::GetSignatureStatusesConfig,
) -> ks_core::Result<crate::SignatureStatusesResult> {
let params = match config.request_params(signatures) {
std::result::Result::Ok(params) => params,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = self
.execute_json_rpc_result_raw("getSignatureStatuses".to_string(), params)
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_signature_statuses_result(&value);
}
/// Returns the block height observed by the node.
pub async fn get_block_height(
&self,
config: &crate::GetBlockHeightConfig,
) -> ks_core::Result<crate::BlockHeightResult> {
let result = self
.execute_json_rpc_result_raw("getBlockHeight".to_string(), config.request_params())
.await;
let value = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::adapt_get_block_height_result(&value);
}
}
impl crate::HttpEndpointPool {
/// Executes `getGenesisHash` through one endpoint selected for the requested role.
pub async fn get_genesis_hash_for_role(
&self,
required_role: &str,
) -> ks_core::Result<crate::GenesisHashResult> {
let client_result = self.select_client_for_role_and_method(required_role, "getGenesisHash");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_genesis_hash().await;
}
/// Executes `getEpochInfo` through one endpoint selected for the requested role.
pub async fn get_epoch_info_for_role(
&self,
required_role: &str,
config: &crate::GetEpochInfoConfig,
) -> ks_core::Result<crate::EpochInfoResult> {
let client_result = self.select_client_for_role_and_method(required_role, "getEpochInfo");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_epoch_info(config).await;
}
/// Executes `getLatestBlockhash` through one endpoint selected for the requested role.
pub async fn get_latest_blockhash_for_role(
&self,
required_role: &str,
config: &crate::GetLatestBlockhashConfig,
) -> ks_core::Result<crate::LatestBlockhashResult> {
let client_result =
self.select_client_for_role_and_method(required_role, "getLatestBlockhash");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_latest_blockhash(config).await;
}
/// Executes `getFeeForMessage` through one endpoint selected for the requested role.
pub async fn get_fee_for_message_for_role(
&self,
required_role: &str,
encoded_message: &str,
config: &crate::GetFeeForMessageConfig,
) -> ks_core::Result<crate::FeeForMessageResult> {
let client_result =
self.select_client_for_role_and_method(required_role, "getFeeForMessage");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_fee_for_message(encoded_message, config).await;
}
/// Executes `simulateTransaction` through one endpoint selected for the requested role.
pub async fn simulate_transaction_for_role(
&self,
required_role: &str,
encoded_transaction: &str,
config: &crate::SimulateTransactionConfig,
) -> ks_core::Result<crate::SimulateTransactionResult> {
let client_result =
self.select_client_for_role_and_method(required_role, "simulateTransaction");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.simulate_transaction(encoded_transaction, config).await;
}
/// Executes `getBalance` through one endpoint selected for the requested role.
pub async fn get_balance_for_role(
&self,
required_role: &str,
pubkey: &ks_lib::MdPubkey,
config: &crate::GetBalanceConfig,
) -> ks_core::Result<crate::BalanceResult> {
let client_result = self.select_client_for_role_and_method(required_role, "getBalance");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_balance(pubkey, config).await;
}
/// Executes `getAccountInfo` through one endpoint selected for the requested role.
pub async fn get_account_info_for_role(
&self,
required_role: &str,
pubkey: &ks_lib::MdPubkey,
config: &crate::GetAccountInfoConfig,
) -> ks_core::Result<crate::AccountInfoResult> {
let client_result = self.select_client_for_role_and_method(required_role, "getAccountInfo");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_account_info(pubkey, config).await;
}
/// Executes `getMinimumBalanceForRentExemption` through one selected endpoint.
pub async fn get_minimum_balance_for_rent_exemption_for_role(
&self,
required_role: &str,
data_length: u64,
config: &crate::GetMinimumBalanceForRentExemptionConfig,
) -> ks_core::Result<crate::MinimumBalanceForRentExemptionResult> {
let client_result = self
.select_client_for_role_and_method(required_role, "getMinimumBalanceForRentExemption");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_minimum_balance_for_rent_exemption(data_length, config).await;
}
/// Executes `requestAirdrop` through one endpoint selected for the requested role.
pub async fn request_airdrop_for_role(
&self,
required_role: &str,
pubkey: &ks_lib::MdPubkey,
lamports: u64,
config: &crate::RequestAirdropConfig,
) -> ks_core::Result<crate::AirdropResult> {
let client_result = self.select_client_for_role_and_method(required_role, "requestAirdrop");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.request_airdrop(pubkey, lamports, config).await;
}
/// Executes `sendTransaction` through one endpoint selected for the requested role.
pub async fn send_transaction_for_role(
&self,
required_role: &str,
encoded_transaction: &str,
expected_signature: &ks_lib::MdSignature,
config: &crate::SendTransactionConfig,
) -> ks_core::Result<crate::SendTransactionResult> {
let client_result =
self.select_client_for_role_and_method(required_role, "sendTransaction");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.send_transaction(encoded_transaction, expected_signature, config).await;
}
/// Executes `getSignatureStatuses` through one endpoint selected for the requested role.
pub async fn get_signature_statuses_for_role(
&self,
required_role: &str,
signatures: &[ks_lib::MdSignature],
config: &crate::GetSignatureStatusesConfig,
) -> ks_core::Result<crate::SignatureStatusesResult> {
let client_result =
self.select_client_for_role_and_method(required_role, "getSignatureStatuses");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_signature_statuses(signatures, config).await;
}
/// Executes `getBlockHeight` through one endpoint selected for the requested role.
pub async fn get_block_height_for_role(
&self,
required_role: &str,
config: &crate::GetBlockHeightConfig,
) -> ks_core::Result<crate::BlockHeightResult> {
let client_result = self.select_client_for_role_and_method(required_role, "getBlockHeight");
let client = match client_result {
std::result::Result::Ok(client) => client,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return client.get_block_height(config).await;
}
/// Polls one signature until the requested commitment, failure, expiration or timeout.
pub async fn confirm_transaction_for_roles(
&self,
status_role: &str,
block_height_role: &str,
cluster: ks_lib::ExApiExecutionCluster,
signature: &ks_lib::MdSignature,
config: &crate::ConfirmTransactionConfig,
) -> ks_core::Result<ks_lib::ExApiExecutionConfirmationResult> {
let status_config =
crate::GetSignatureStatusesConfig::new(config.search_transaction_history);
let block_height_config =
crate::GetBlockHeightConfig::new(config.commitment, config.min_context_slot);
let mut attempts = 0_u32;
let mut last_slot = std::option::Option::None;
let mut last_observed_block_height = std::option::Option::None;
while attempts < config.max_attempts {
attempts = attempts.saturating_add(1);
let statuses_result = self
.get_signature_statuses_for_role(
status_role,
std::slice::from_ref(signature),
&status_config,
)
.await;
let statuses = match statuses_result {
std::result::Result::Ok(statuses) => statuses,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if statuses.statuses.len() != 1 {
return std::result::Result::Err(ks_core::Error::new(
"execution_confirmation_status_count_mismatch",
format!(
"getSignatureStatuses returned {} entries for one requested signature",
statuses.statuses.len()
),
));
}
let status = match statuses.statuses.first() {
std::option::Option::Some(status) => status.as_ref(),
std::option::Option::None => std::option::Option::None,
};
if let std::option::Option::Some(status) = status {
last_slot = std::option::Option::Some(status.slot);
if let std::option::Option::Some(error) = &status.error {
return std::result::Result::Ok(confirmation_result(
cluster,
signature,
ks_lib::ExApiExecutionConfirmationStatus::Failed,
last_slot,
attempts,
last_observed_block_height,
std::option::Option::Some(error.to_string()),
));
}
let observed_commitment = match status.confirmation_status {
std::option::Option::Some(commitment) => commitment,
std::option::Option::None => crate::RpcCommitmentLevel::Processed,
};
if commitment_reached(observed_commitment, config.commitment) {
let execution_status = execution_confirmation_status(observed_commitment);
tracing::info!(
target: crate::TRACING_TARGET,
action = "confirm_transaction",
signature = %signature.0,
attempts,
slot = status.slot,
confirmation_status = observed_commitment.as_str(),
"transaction reached requested commitment"
);
return std::result::Result::Ok(confirmation_result(
cluster,
signature,
execution_status,
last_slot,
attempts,
last_observed_block_height,
std::option::Option::None,
));
}
}
if let std::option::Option::Some(last_valid_block_height) =
config.last_valid_block_height
{
let height_result =
self.get_block_height_for_role(block_height_role, &block_height_config).await;
let height = match height_result {
std::result::Result::Ok(height) => height,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
last_observed_block_height = std::option::Option::Some(height.block_height);
if height.block_height > last_valid_block_height {
return std::result::Result::Ok(confirmation_result(
cluster,
signature,
ks_lib::ExApiExecutionConfirmationStatus::Expired,
last_slot,
attempts,
last_observed_block_height,
std::option::Option::Some(format!(
"recent blockhash expired at block height {last_valid_block_height}; observed {observed}",
observed = height.block_height
)),
));
}
}
if attempts < config.max_attempts {
tokio::time::sleep(std::time::Duration::from_millis(config.poll_interval_ms)).await;
}
}
return std::result::Result::Ok(confirmation_result(
cluster,
signature,
ks_lib::ExApiExecutionConfirmationStatus::TimedOut,
last_slot,
attempts,
last_observed_block_height,
std::option::Option::Some(format!(
"transaction did not reach '{}' commitment after {} attempts",
config.commitment.as_str(),
attempts
)),
));
}
}
fn validate_base64_payload(value: &str, field_name: &str) -> ks_core::Result<()> {
if value.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(format!(
"{field_name} must not be empty"
)));
}
if value.len() > crate::MAX_EXECUTION_RPC_BASE64_LENGTH {
return std::result::Result::Err(ks_core::Error::config(format!(
"{field_name} exceeds the local encoded length limit of {} bytes",
crate::MAX_EXECUTION_RPC_BASE64_LENGTH
)));
}
let decode_result = base64::engine::general_purpose::STANDARD.decode(value);
let decoded = match decode_result {
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::config(format!(
"{field_name} is not valid base64: {error}"
)));
},
};
if decoded.is_empty() {
return std::result::Result::Err(ks_core::Error::config(format!(
"{field_name} must decode to at least one byte"
)));
}
return std::result::Result::Ok(());
}
#[derive(Clone, Debug, serde::Deserialize)]
struct RpcContextValue<T> {
context: crate::RpcResponseContext,
value: T,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcLatestBlockhashValue {
blockhash: std::string::String,
last_valid_block_height: u64,
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcSimulationValue {
#[serde(default, rename = "err")]
error: std::option::Option<serde_json::Value>,
#[serde(default)]
logs: std::option::Option<std::vec::Vec<std::string::String>>,
#[serde(default)]
units_consumed: std::option::Option<u64>,
#[serde(default)]
fee: std::option::Option<u64>,
#[serde(default)]
loaded_accounts_data_size: std::option::Option<u64>,
#[serde(default)]
replacement_blockhash: std::option::Option<crate::SimulationReplacementBlockhash>,
#[serde(default)]
return_data: std::option::Option<serde_json::Value>,
#[serde(default)]
inner_instructions: std::option::Option<serde_json::Value>,
#[serde(default)]
accounts: std::option::Option<std::vec::Vec<std::option::Option<serde_json::Value>>>,
}
fn adapt_signature_result(
source: &serde_json::Value,
field_name: &str,
) -> ks_core::Result<ks_lib::MdSignature> {
let signature = match source.as_str() {
std::option::Option::Some(signature) => signature.to_string(),
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::json(format!(
"{field_name} must be a base58 signature string"
)));
},
};
let validation_result =
crate::validate_transaction_signature_text(signature.as_str(), field_name);
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(ks_lib::MdSignature(signature));
}
fn commitment_rank(commitment: crate::RpcCommitmentLevel) -> u8 {
return match commitment {
crate::RpcCommitmentLevel::Processed => 0,
crate::RpcCommitmentLevel::Confirmed => 1,
crate::RpcCommitmentLevel::Finalized => 2,
};
}
fn commitment_reached(
observed: crate::RpcCommitmentLevel,
required: crate::RpcCommitmentLevel,
) -> bool {
return commitment_rank(observed) >= commitment_rank(required);
}
fn execution_confirmation_status(
commitment: crate::RpcCommitmentLevel,
) -> ks_lib::ExApiExecutionConfirmationStatus {
return match commitment {
crate::RpcCommitmentLevel::Processed => ks_lib::ExApiExecutionConfirmationStatus::Processed,
crate::RpcCommitmentLevel::Confirmed => ks_lib::ExApiExecutionConfirmationStatus::Confirmed,
crate::RpcCommitmentLevel::Finalized => ks_lib::ExApiExecutionConfirmationStatus::Finalized,
};
}
fn confirmation_result(
cluster: ks_lib::ExApiExecutionCluster,
signature: &ks_lib::MdSignature,
status: ks_lib::ExApiExecutionConfirmationStatus,
slot: std::option::Option<u64>,
attempts: u32,
last_observed_block_height: std::option::Option<u64>,
error: std::option::Option<std::string::String>,
) -> ks_lib::ExApiExecutionConfirmationResult {
return ks_lib::ExApiExecutionConfirmationResult {
cluster,
signature: signature.clone(),
status,
slot,
attempts,
last_observed_block_height,
error,
};
}
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct RpcSignatureStatusValue {
slot: u64,
#[serde(default)]
confirmations: std::option::Option<u64>,
#[serde(default, rename = "err")]
error: std::option::Option<serde_json::Value>,
#[serde(default)]
confirmation_status: std::option::Option<crate::RpcCommitmentLevel>,
}
#[cfg(test)]
mod tests {
use base64::Engine; // rust-rules: trait-import
fn valid_hash(byte: u8) -> std::string::String {
return bs58::encode([byte; 32]).into_string();
}
fn encoded_bytes(bytes: &[u8]) -> std::string::String {
return base64::engine::general_purpose::STANDARD.encode(bytes);
}
#[test]
fn genesis_hash_classifies_official_public_clusters() {
let devnet = crate::adapt_get_genesis_hash_result(&serde_json::Value::String(
crate::DEVNET_GENESIS_HASH.to_string(),
))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(
devnet.classified_cluster,
std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet)
);
let mainnet = crate::adapt_get_genesis_hash_result(&serde_json::Value::String(
crate::MAINNET_GENESIS_HASH.to_string(),
))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(
mainnet.classified_cluster,
std::option::Option::Some(ks_lib::ExApiExecutionCluster::Mainnet)
);
let local = crate::adapt_get_genesis_hash_result(&serde_json::Value::String(valid_hash(7)))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(local.classified_cluster, std::option::Option::None);
}
#[test]
fn latest_blockhash_request_and_result_are_typed() {
let config = crate::GetLatestBlockhashConfig::new(
crate::RpcCommitmentLevel::Processed,
std::option::Option::Some(41),
);
assert_eq!(
config.request_params(),
std::vec![serde_json::json!({
"commitment": "processed",
"minContextSlot": 41
})]
);
let blockhash = valid_hash(9);
let result = crate::adapt_get_latest_blockhash_result(&serde_json::json!({
"context": {"slot": 42, "apiVersion": "4.1.1"},
"value": {"blockhash": blockhash, "lastValidBlockHeight": 99}
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(result.context.slot, 42);
assert_eq!(result.last_valid_block_height, 99);
}
#[test]
fn fee_request_rejects_invalid_base64_and_accepts_null_fee() {
let config = crate::GetFeeForMessageConfig::confirmed();
assert!(config.request_params("not base64").is_err());
let result = crate::adapt_get_fee_for_message_result(&serde_json::json!({
"context": {"slot": 51},
"value": null
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(result.fee_lamports, std::option::Option::None);
}
#[test]
fn simulation_configuration_builds_unsigned_request() {
let exact = crate::SimulateTransactionConfig::unsigned_exact();
assert!(!exact.signature_verification);
assert!(!exact.replace_recent_blockhash);
let account = ks_lib::MdPubkey(valid_hash(11));
let accounts = crate::SimulationAccountsConfig::new(std::vec![account])
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
let config = crate::SimulateTransactionConfig::new(
crate::RpcCommitmentLevel::Confirmed,
false,
true,
std::option::Option::Some(60),
true,
std::option::Option::Some(accounts),
)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
let encoded = encoded_bytes(&[1, 2, 3]);
let params = config
.request_params(encoded.as_str())
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(params[0], serde_json::Value::String(encoded));
assert_eq!(params[1]["encoding"], "base64");
assert_eq!(params[1]["sigVerify"], false);
assert_eq!(params[1]["replaceRecentBlockhash"], true);
assert_eq!(params[1]["innerInstructions"], true);
assert_eq!(params[1]["minContextSlot"], 60);
assert_eq!(params[1]["accounts"]["encoding"], "base64");
}
#[test]
fn simulation_configuration_rejects_signature_verification_with_replacement() {
let result = crate::SimulateTransactionConfig::new(
crate::RpcCommitmentLevel::Confirmed,
true,
true,
std::option::Option::None,
false,
std::option::Option::None,
);
assert!(result.is_err());
}
#[test]
fn simulation_result_preserves_runtime_diagnostics() {
let replacement = valid_hash(13);
let result = crate::adapt_simulate_transaction_result(&serde_json::json!({
"context": {"slot": 70, "apiVersion": "4.1.1"},
"value": {
"err": null,
"logs": ["Program 111 invoke [1]", "Program 111 success"],
"unitsConsumed": 1714,
"fee": 5000,
"loadedAccountsDataSize": 413,
"replacementBlockhash": {
"blockhash": replacement,
"lastValidBlockHeight": 100
},
"returnData": null,
"innerInstructions": null,
"accounts": null
}
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert!(result.success);
assert_eq!(result.units_consumed, std::option::Option::Some(1714));
assert_eq!(result.fee_lamports, std::option::Option::Some(5000));
assert_eq!(result.logs.len(), 2);
}
#[test]
fn simulation_result_converts_to_common_execution_contract() {
let rpc = crate::SimulateTransactionResult {
context: crate::RpcResponseContext {
slot: 90,
api_version: std::option::Option::None,
},
success: false,
error: std::option::Option::Some(
serde_json::json!({"InstructionError": [0, "Custom"]}),
),
logs: std::vec!["failed".to_string()],
units_consumed: std::option::Option::Some(12),
fee_lamports: std::option::Option::None,
loaded_accounts_data_size: std::option::Option::None,
replacement_blockhash: std::option::Option::Some(
crate::SimulationReplacementBlockhash {
blockhash: valid_hash(14),
last_valid_block_height: 101,
},
),
return_data: std::option::Option::Some(serde_json::json!({
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"data": ["AQ==", "base64"]
})),
inner_instructions: std::option::Option::None,
accounts: std::option::Option::None,
};
let fee = crate::FeeForMessageResult {
context: crate::RpcResponseContext {
slot: 90,
api_version: std::option::Option::None,
},
fee_lamports: std::option::Option::Some(5000),
};
let execution = rpc.to_execution_result(
ks_lib::ExApiExecutionCluster::Devnet,
ks_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(3),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
assert!(execution.simulated);
assert!(!execution.success);
assert_eq!(execution.estimated_fee_lamports, std::option::Option::Some(5000));
assert_eq!(execution.replacement_last_valid_block_height, std::option::Option::Some(101));
assert!(execution.replacement_blockhash.is_some());
assert_eq!(
execution
.return_data
.as_ref()
.and_then(|value| return value.get("programId"))
.and_then(serde_json::Value::as_str),
std::option::Option::Some("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
);
assert!(execution.error.is_some());
}
#[test]
fn adapters_reject_invalid_hash_lengths() {
let invalid = bs58::encode([1_u8; 31]).into_string();
let result = crate::adapt_get_latest_blockhash_result(&serde_json::json!({
"context": {"slot": 1},
"value": {"blockhash": invalid, "lastValidBlockHeight": 2}
}));
assert!(result.is_err());
}
fn valid_signature(byte: u8) -> std::string::String {
return bs58::encode([byte; 64]).into_string();
}
fn execution_config() -> ks_config::ExecutionConfig {
return ks_config::ExecutionConfig {
dry_run_default: true,
require_simulation: true,
require_operator_confirmation: true,
localnet_max_spend_lamports: 1_000_000,
devnet_max_spend_lamports: 1_000_000,
testnet_max_spend_lamports: 0,
mainnet_max_spend_lamports: 0,
max_fee_lamports: 10_000,
max_compute_unit_price_micro_lamports: 5,
recent_blockhash_max_age_slots: 150,
send_max_retries: 7,
confirmation_poll_interval_ms: 250,
confirmation_max_attempts: 80,
devnet_airdrop_max_lamports: 2_000_000_000,
};
}
#[test]
fn epoch_info_request_and_result_are_typed() {
let config = crate::GetEpochInfoConfig::new(
crate::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(44),
);
let params = config.request_params();
assert_eq!(params[0]["commitment"], "confirmed");
assert_eq!(params[0]["minContextSlot"], 44);
let epoch = crate::adapt_get_epoch_info_result(&serde_json::json!({
"absoluteSlot": 500,
"blockHeight": 480,
"epoch": 9,
"slotIndex": 20,
"slotsInEpoch": 100,
"transactionCount": null
}))
.unwrap_or_else(|error| panic!("unexpected epoch error: {error}"));
assert_eq!(epoch.absolute_slot, 500);
assert_eq!(epoch.epoch, 9);
assert_eq!(epoch.transaction_count, std::option::Option::None);
assert!(
crate::adapt_get_epoch_info_result(&serde_json::json!({
"absoluteSlot": "bad"
}))
.is_err()
);
}
#[test]
fn balance_and_airdrop_requests_are_typed() {
let pubkey = ks_lib::MdPubkey(valid_hash(20));
let balance_config = crate::GetBalanceConfig::confirmed();
let balance_params = balance_config
.request_params(&pubkey)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(balance_params[0], pubkey.0);
assert_eq!(balance_params[1]["commitment"], "confirmed");
let balance = crate::adapt_get_balance_result(&serde_json::json!({
"context": {"slot": 101},
"value": 9000
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(balance.lamports, 9000);
let airdrop_config = crate::RequestAirdropConfig::confirmed();
let airdrop_params = airdrop_config
.request_params(&pubkey, 1_000_000)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(airdrop_params[1], 1_000_000);
let signature = valid_signature(21);
let airdrop = crate::adapt_request_airdrop_result(&serde_json::json!(signature))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(airdrop.signature.0, signature);
assert!(airdrop_config.request_params(&pubkey, 0).is_err());
}
#[test]
fn account_existence_and_rent_requests_are_typed() {
let pubkey = ks_lib::MdPubkey(valid_hash(22));
let account_config = crate::GetAccountInfoConfig::confirmed();
let account_params = account_config
.request_params(&pubkey)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(account_params[0], pubkey.0);
assert_eq!(account_params[1]["encoding"], "base64");
assert_eq!(account_params[1]["dataSlice"]["length"], 0);
let missing = crate::adapt_get_account_info_result(&serde_json::json!({
"context": {"slot": 102},
"value": null
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert!(missing.account.is_none());
let owner = valid_hash(23);
let existing = crate::adapt_get_account_info_result(&serde_json::json!({
"context": {"slot": 103},
"value": {
"lamports": 890880,
"owner": owner,
"executable": false,
"rentEpoch": 0,
"space": 0,
"data": ["", "base64"]
}
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(
existing.account.as_ref().map(|account| return account.lamports),
std::option::Option::Some(890880)
);
assert_eq!(
existing.account.as_ref().map(|account| return account.space),
std::option::Option::Some(0)
);
assert!(
existing
.account
.as_ref()
.map(|account| return account.data.is_empty())
.unwrap_or(false)
);
let complete_config = crate::GetAccountInfoConfig::confirmed_with_data(80)
.unwrap_or_else(|error| panic!("unexpected complete-data config error: {error}"));
let complete_params = complete_config
.request_params(&pubkey)
.unwrap_or_else(|error| panic!("unexpected complete-data request error: {error}"));
assert!(complete_params[1].get("dataSlice").is_none());
let complete_bytes = vec![7_u8; 80];
let complete = crate::adapt_get_account_info_result_with_data_limit(
&serde_json::json!({
"context": {"slot": 104},
"value": {
"lamports": 1_500_000,
"owner": owner,
"executable": false,
"rentEpoch": 0,
"space": 80,
"data": [encoded_bytes(complete_bytes.as_slice()), "base64"]
}
}),
std::option::Option::Some(80),
)
.unwrap_or_else(|error| panic!("unexpected complete-data adapter error: {error}"));
assert_eq!(
complete.account.as_ref().map(|account| return account.data.clone()),
std::option::Option::Some(complete_bytes)
);
assert!(crate::GetAccountInfoConfig::confirmed_with_data(0).is_err());
assert!(
crate::GetAccountInfoConfig::confirmed_with_data(
crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES + 1,
)
.is_err()
);
assert!(
crate::adapt_get_account_info_result_with_data_limit(
&serde_json::json!({
"context": {"slot": 105},
"value": {
"lamports": 1,
"owner": valid_hash(24),
"executable": false,
"rentEpoch": 0,
"space": 81,
"data": [encoded_bytes(&[0_u8; 81]), "base64"]
}
}),
std::option::Option::Some(80),
)
.is_err()
);
let rent_config = crate::GetMinimumBalanceForRentExemptionConfig::confirmed();
assert_eq!(rent_config.request_params(0)[0], 0);
let rent = crate::adapt_get_minimum_balance_for_rent_exemption_result(
&serde_json::json!(890880),
0,
)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(rent.minimum_balance_lamports, 890880);
}
#[test]
fn execution_settings_build_submission_and_confirmation_policies() {
let execution = execution_config();
let send = crate::SendTransactionConfig::from_execution_config(
&execution,
std::option::Option::Some(44),
)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert!(!send.skip_preflight);
assert_eq!(send.max_retries, std::option::Option::Some(7));
assert_eq!(send.min_context_slot, std::option::Option::Some(44));
let confirmation = crate::ConfirmTransactionConfig::from_execution_config(
&execution,
std::option::Option::Some(900),
std::option::Option::Some(44),
)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(confirmation.poll_interval_ms, 250);
assert_eq!(confirmation.max_attempts, 80);
assert_eq!(confirmation.last_valid_block_height, std::option::Option::Some(900));
assert_eq!(confirmation.min_context_slot, std::option::Option::Some(44));
}
#[test]
fn send_transaction_requires_preflight_and_matching_signature() {
let invalid = crate::SendTransactionConfig::new(
true,
crate::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(3),
std::option::Option::None,
);
assert!(invalid.is_err());
let config = crate::SendTransactionConfig::confirmed();
let encoded = encoded_bytes(&[4, 5, 6]);
let params = config
.request_params(encoded.as_str())
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(params[1]["encoding"], "base64");
assert_eq!(params[1]["skipPreflight"], false);
assert_eq!(params[1]["preflightCommitment"], "confirmed");
assert_eq!(params[1]["maxRetries"], 3);
let expected = ks_lib::MdSignature(valid_signature(22));
let accepted =
crate::adapt_send_transaction_result(&serde_json::json!(expected.0.clone()), &expected)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(accepted.signature, expected);
let different = serde_json::json!(valid_signature(23));
assert!(crate::adapt_send_transaction_result(&different, &expected).is_err());
}
#[test]
fn signature_statuses_preserve_order_and_runtime_errors() {
let signature = ks_lib::MdSignature(valid_signature(24));
let config = crate::GetSignatureStatusesConfig::new(true);
let params = config
.request_params(std::slice::from_ref(&signature))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(params[0][0], signature.0);
assert_eq!(params[1]["searchTransactionHistory"], true);
let statuses = crate::adapt_get_signature_statuses_result(&serde_json::json!({
"context": {"slot": 120},
"value": [
{
"slot": 118,
"confirmations": 2,
"err": null,
"status": {"Ok": null},
"confirmationStatus": "confirmed"
},
null,
{
"slot": 119,
"confirmations": null,
"err": {"InstructionError": [0, "Custom"]},
"confirmationStatus": "finalized"
}
]
}))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(statuses.statuses.len(), 3);
let confirmed = statuses.statuses[0]
.as_ref()
.unwrap_or_else(|| panic!("missing confirmed status"));
assert_eq!(
confirmed.confirmation_status,
std::option::Option::Some(crate::RpcCommitmentLevel::Confirmed)
);
assert!(statuses.statuses[1].is_none());
let failed =
statuses.statuses[2].as_ref().unwrap_or_else(|| panic!("missing failed status"));
assert!(failed.error.is_some());
}
#[test]
fn signature_status_request_is_bounded_to_official_limit() {
let config = crate::GetSignatureStatusesConfig::new(false);
assert!(config.request_params(&[]).is_err());
let signatures = (0..=crate::MAX_SIGNATURE_STATUS_COUNT)
.map(|index| {
return ks_lib::MdSignature(valid_signature((index % 251) as u8));
})
.collect::<std::vec::Vec<_>>();
assert!(config.request_params(signatures.as_slice()).is_err());
}
#[test]
fn confirmation_policy_and_commitment_order_are_bounded() {
assert!(crate::ConfirmTransactionConfig::confirmed(100, 0, 10).is_err());
assert!(crate::ConfirmTransactionConfig::confirmed(100, 500, 0).is_err());
let config = crate::ConfirmTransactionConfig::confirmed(100, 500, 20)
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(config.last_valid_block_height, std::option::Option::Some(100));
assert!(super::commitment_reached(
crate::RpcCommitmentLevel::Finalized,
crate::RpcCommitmentLevel::Confirmed,
));
assert!(!super::commitment_reached(
crate::RpcCommitmentLevel::Processed,
crate::RpcCommitmentLevel::Confirmed,
));
let height = crate::adapt_get_block_height_result(&serde_json::json!(121))
.unwrap_or_else(|error| panic!("unexpected error: {error}"));
assert_eq!(height.block_height, 121);
}
}