2705 lines
108 KiB
Rust
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);
|
|
}
|
|
}
|