// file: kb-onchain-transport/src/execution_rpc.rs // version: 10 //! 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, } /// 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, } /// 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, } impl crate::GetEpochInfoConfig { /// Creates an epoch information request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, min_context_slot: std::option::Option, ) -> 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 { let mut config = serde_json::Map::::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, } /// 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, } impl crate::GetLatestBlockhashConfig { /// Creates a blockhash request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, min_context_slot: std::option::Option, ) -> 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 { let mut config = serde_json::Map::::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, } impl crate::GetFeeForMessageConfig { /// Creates a fee request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, min_context_slot: std::option::Option, ) -> 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, ) -> kb_core::Result> { 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::::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, } /// Optional accounts requested from `simulateTransaction`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SimulationAccountsConfig { /// Account addresses returned after simulation. pub addresses: std::vec::Vec, } impl crate::SimulationAccountsConfig { /// Creates a bounded account-return request using base64 account data. pub fn new(addresses: std::vec::Vec) -> kb_core::Result { if addresses.is_empty() { return std::result::Result::Err(kb_core::Error::config( "simulateTransaction accounts must not be empty", )); } if addresses.len() > crate::MAX_SIMULATION_ACCOUNT_COUNT { return std::result::Result::Err(kb_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::>(); 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, /// Whether inner instructions should be returned. pub inner_instructions: bool, /// Optional post-simulation account values to return. pub accounts: std::option::Option, } 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, inner_instructions: bool, accounts: std::option::Option, ) -> kb_core::Result { if signature_verification && replace_recent_blockhash { return std::result::Result::Err(kb_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, ) -> kb_core::Result> { 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::::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, /// Runtime logs in execution order. pub logs: std::vec::Vec, /// Compute units consumed when reported. pub units_consumed: std::option::Option, /// Fee reported by simulation when supported by the node. pub fee_lamports: std::option::Option, /// Total loaded account data size when reported. pub loaded_accounts_data_size: std::option::Option, /// Replacement recent blockhash when requested. pub replacement_blockhash: std::option::Option, /// Program return data retained as provider-neutral JSON. pub return_data: std::option::Option, /// Inner instructions retained as provider-neutral JSON. pub inner_instructions: std::option::Option, /// Requested account snapshots retained as provider-neutral JSON. pub accounts: std::option::Option>>, } impl crate::SimulateTransactionResult { /// Converts this RPC result into the common execution simulation contract. pub fn to_execution_result( &self, cluster: kb_lib::ExApiExecutionCluster, blockhash_kind: kb_lib::ExApiExecutionBlockhashKind, blockhash_age_slots: std::option::Option, nonce_account: std::option::Option, nonce_authority: std::option::Option, fee_fallback: std::option::Option<&crate::FeeForMessageResult>, ) -> kb_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 kb_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, } impl crate::GetBalanceConfig { /// Creates a balance request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, min_context_slot: std::option::Option, ) -> 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: &kb_lib::MdPubkey, ) -> kb_core::Result> { 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::::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, /// Maximum complete decoded account-data length, or `None` for metadata-only requests. pub max_data_bytes: std::option::Option, } impl crate::GetAccountInfoConfig { /// Creates an account information request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, min_context_slot: std::option::Option, ) -> 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, max_data_bytes: usize, ) -> kb_core::Result { if max_data_bytes == 0 || max_data_bytes > crate::MAX_COMPLETE_ACCOUNT_DATA_BYTES { return std::result::Result::Err(kb_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) -> kb_core::Result { return Self::new_with_data( crate::RpcCommitmentLevel::Confirmed, std::option::Option::None, max_data_bytes, ); } fn request_params( &self, pubkey: &kb_lib::MdPubkey, ) -> kb_core::Result> { 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(kb_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::::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: kb_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, } /// 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, } /// 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 { 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, } impl crate::RequestAirdropConfig { /// Creates an airdrop request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, recent_blockhash: std::option::Option, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, lamports: u64, ) -> kb_core::Result> { 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(kb_core::Error::config( "requestAirdrop lamports must be greater than zero", )); } let mut config = serde_json::Map::::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: kb_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, /// Optional minimum context slot. pub min_context_slot: std::option::Option, } impl crate::SendTransactionConfig { /// Creates a validated transaction submission configuration. pub fn new( skip_preflight: bool, preflight_commitment: crate::RpcCommitmentLevel, max_retries: std::option::Option, min_context_slot: std::option::Option, ) -> kb_core::Result { if skip_preflight { return std::result::Result::Err(kb_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: &kb_config::ExecutionConfig, min_context_slot: std::option::Option, ) -> kb_core::Result { 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, ) -> kb_core::Result> { 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::::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: kb_lib::MdSignature, } impl crate::SendTransactionResult { /// Converts the RPC result into the common execution send contract. pub fn to_execution_result( &self, cluster: kb_lib::ExApiExecutionCluster, ) -> kb_lib::ExApiExecutionSendResult { return kb_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: &[kb_lib::MdSignature], ) -> kb_core::Result> { if signatures.is_empty() { return std::result::Result::Err(kb_core::Error::config( "getSignatureStatuses requires at least one signature", )); } if signatures.len() > crate::MAX_SIGNATURE_STATUS_COUNT { return std::result::Result::Err(kb_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, /// Runtime transaction error when execution failed. pub error: std::option::Option, /// Highest commitment reported by the node. pub confirmation_status: std::option::Option, } /// 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>, } /// 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, } impl crate::GetBlockHeightConfig { /// Creates a block-height request configuration. pub fn new( commitment: crate::RpcCommitmentLevel, min_context_slot: std::option::Option, ) -> Self { return Self { commitment, min_context_slot }; } fn request_params(&self) -> std::vec::Vec { let mut config = serde_json::Map::::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, /// Optional minimum context slot for block-height checks. pub min_context_slot: std::option::Option, } 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, min_context_slot: std::option::Option, ) -> kb_core::Result { if poll_interval_ms == 0 || poll_interval_ms > crate::MAX_CONFIRMATION_POLL_INTERVAL_MS { return std::result::Result::Err(kb_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(kb_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, ) -> kb_core::Result { 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: &kb_config::ExecutionConfig, last_valid_block_height: std::option::Option, min_context_slot: std::option::Option, ) -> kb_core::Result { 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 { return match genesis_hash { crate::DEVNET_GENESIS_HASH => { std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet) }, crate::TESTNET_GENESIS_HASH => { std::option::Option::Some(kb_lib::ExApiExecutionCluster::Testnet) }, crate::MAINNET_GENESIS_HASH => { std::option::Option::Some(kb_lib::ExApiExecutionCluster::Mainnet) }, _ => std::option::Option::None, }; } /// Adapts a raw `getGenesisHash` result. pub fn adapt_get_genesis_hash_result( source: &serde_json::Value, ) -> kb_core::Result { let genesis_hash = match source.as_str() { std::option::Option::Some(value) => value.to_string(), std::option::Option::None => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { let parse_result = serde_json::from_value::>(source.clone()); let parsed = match parse_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { let parse_result = serde_json::from_value::>>(source.clone()); let parsed = match parse_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { let parse_result = serde_json::from_value::>(source.clone()); let parsed = match parse_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { let parse_result = serde_json::from_value::>(source.clone()); let parsed = match parse_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { 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, ) -> kb_core::Result { let context = match source.get("context") { std::option::Option::Some(value) => { let parse_result = serde_json::from_value::(value.clone()); match parse_result { std::result::Result::Ok(context) => context, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::json(format!( "cannot parse getAccountInfo context: {error}" ))); }, } }, std::option::Option::None => { return std::result::Result::Err(kb_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(kb_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(kb_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(kb_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(kb_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(kb_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(kb_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: kb_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, ) -> kb_core::Result> { 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(kb_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(kb_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(kb_core::Error::json( "getAccountInfo account data encoding must be a string", )); }, }; if encoding != "base64" { return std::result::Result::Err(kb_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(kb_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(kb_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(kb_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(kb_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(kb_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(kb_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(kb_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, ) -> kb_core::Result { let minimum_balance_lamports = match source.as_u64() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { 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: &kb_lib::MdSignature, ) -> kb_core::Result { 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(kb_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, ) -> kb_core::Result { let parse_result = serde_json::from_value::< RpcContextValue>>, >(source.clone()); let parsed = match parse_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_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, ) -> kb_core::Result { let parse_result = serde_json::from_value::(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(kb_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, ) -> kb_core::Result { let block_height = match source.as_u64() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_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) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, config: &crate::GetBalanceConfig, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, config: &crate::GetAccountInfoConfig, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, lamports: u64, config: &crate::RequestAirdropConfig, ) -> kb_core::Result { 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: &kb_lib::MdSignature, config: &crate::SendTransactionConfig, ) -> kb_core::Result { 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: &[kb_lib::MdSignature], config: &crate::GetSignatureStatusesConfig, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, config: &crate::GetBalanceConfig, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, config: &crate::GetAccountInfoConfig, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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: &kb_lib::MdPubkey, lamports: u64, config: &crate::RequestAirdropConfig, ) -> kb_core::Result { 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: &kb_lib::MdSignature, config: &crate::SendTransactionConfig, ) -> kb_core::Result { 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: &[kb_lib::MdSignature], config: &crate::GetSignatureStatusesConfig, ) -> kb_core::Result { 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, ) -> kb_core::Result { 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: kb_lib::ExApiExecutionCluster, signature: &kb_lib::MdSignature, config: &crate::ConfirmTransactionConfig, ) -> kb_core::Result { 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(kb_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, kb_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, kb_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, kb_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) -> kb_core::Result<()> { if value.trim().is_empty() { return std::result::Result::Err(kb_core::Error::config(format!( "{field_name} must not be empty" ))); } if value.len() > crate::MAX_EXECUTION_RPC_BASE64_LENGTH { return std::result::Result::Err(kb_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(kb_core::Error::config(format!( "{field_name} is not valid base64: {error}" ))); }, }; if decoded.is_empty() { return std::result::Result::Err(kb_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 { 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(default)] logs: std::option::Option>, #[serde(default)] units_consumed: std::option::Option, #[serde(default)] fee: std::option::Option, #[serde(default)] loaded_accounts_data_size: std::option::Option, #[serde(default)] replacement_blockhash: std::option::Option, #[serde(default)] return_data: std::option::Option, #[serde(default)] inner_instructions: std::option::Option, #[serde(default)] accounts: std::option::Option>>, } fn adapt_signature_result( source: &serde_json::Value, field_name: &str, ) -> kb_core::Result { let signature = match source.as_str() { std::option::Option::Some(signature) => signature.to_string(), std::option::Option::None => { return std::result::Result::Err(kb_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(kb_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, ) -> kb_lib::ExApiExecutionConfirmationStatus { return match commitment { crate::RpcCommitmentLevel::Processed => kb_lib::ExApiExecutionConfirmationStatus::Processed, crate::RpcCommitmentLevel::Confirmed => kb_lib::ExApiExecutionConfirmationStatus::Confirmed, crate::RpcCommitmentLevel::Finalized => kb_lib::ExApiExecutionConfirmationStatus::Finalized, }; } fn confirmation_result( cluster: kb_lib::ExApiExecutionCluster, signature: &kb_lib::MdSignature, status: kb_lib::ExApiExecutionConfirmationStatus, slot: std::option::Option, attempts: u32, last_observed_block_height: std::option::Option, error: std::option::Option, ) -> kb_lib::ExApiExecutionConfirmationResult { return kb_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, #[serde(default, rename = "err")] error: std::option::Option, #[serde(default)] confirmation_status: std::option::Option, } #[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(kb_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(kb_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 = kb_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( kb_lib::ExApiExecutionCluster::Devnet, kb_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() -> kb_config::ExecutionConfig { return kb_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 = kb_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 = kb_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 = kb_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 = kb_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 kb_lib::MdSignature(valid_signature((index % 251) as u8)); }) .collect::>(); 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); } }