// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs // version: 10 const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128; const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000; const MAX_SIGNATURE_STATUSES: usize = 256; /// Binary encoding accepted for serialized transaction input payloads. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SolanaTransactionBinaryEncoding { /// Base58 text encoding. Base58, /// Base64 text encoding. Base64, } impl SolanaTransactionBinaryEncoding { /// Returns the Solana JSON-RPC encoding string. #[must_use] pub const fn as_str(self) -> &'static str { return match self { Self::Base58 => "base58", Self::Base64 => "base64", }; } fn from_wire(value: &str) -> std::option::Option { return match value { "base58" => std::option::Option::Some(Self::Base58), "base64" => std::option::Option::Some(Self::Base64), _ => std::option::Option::None, }; } } /// Encoding accepted by `getTransaction`, including its retained legacy `binary` alias. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SolanaTransactionEncoding { /// Legacy base58-compatible `binary` alias retained by Solana RPC for backwards compatibility. Binary, /// Base58 encoded transaction bytes. Base58, /// Base64 encoded transaction bytes. Base64, /// Raw JSON transaction representation. Json, /// Parsed JSON transaction representation. JsonParsed, } impl SolanaTransactionEncoding { /// Returns the Solana JSON-RPC encoding string. #[must_use] pub const fn as_str(self) -> &'static str { return match self { Self::Binary => "binary", Self::Base58 => "base58", Self::Base64 => "base64", Self::Json => "json", Self::JsonParsed => "jsonParsed", }; } } /// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`. #[derive(Clone, Debug, PartialEq)] pub enum SolanaWireField { /// The field was not present in the decoded wire object. Omitted, /// The field was explicitly present with JSON `null`. Null, /// The field was present with a concrete value. Value(T), } impl SolanaWireField { /// Returns whether the field was omitted from the wire object. #[must_use] pub const fn is_omitted(&self) -> bool { return match self { Self::Omitted => true, Self::Null | Self::Value(_) => false, }; } /// Returns whether the field was explicitly JSON `null`. #[must_use] pub const fn is_null(&self) -> bool { return match self { Self::Null => true, Self::Omitted | Self::Value(_) => false, }; } /// Returns the concrete value when the field was present and non-null. #[must_use] pub const fn value(&self) -> std::option::Option<&T> { return match self { Self::Value(value) => std::option::Option::Some(value), Self::Omitted | Self::Null => std::option::Option::None, }; } } impl std::default::Default for SolanaWireField { fn default() -> Self { return Self::Omitted; } } impl<'de, T> serde::Deserialize<'de> for SolanaWireField where T: serde::Deserialize<'de>, { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { let decoded = as serde::Deserialize>::deserialize(deserializer); return match decoded { std::result::Result::Ok(std::option::Option::Some(value)) => std::result::Result::Ok(Self::Value(value)), std::result::Result::Ok(std::option::Option::None) => std::result::Result::Ok(Self::Null), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Modern configuration object accepted by `getTransaction`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaGetTransactionConfig { commitment: std::option::Option, encoding: std::option::Option, max_supported_transaction_version: std::option::Option, } impl SolanaGetTransactionConfig { /// Creates a modern `getTransaction` configuration. #[must_use] pub const fn new( commitment: std::option::Option, encoding: std::option::Option, max_supported_transaction_version: std::option::Option, ) -> Self { return Self { commitment, encoding, max_supported_transaction_version }; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional transaction response encoding. #[must_use] pub const fn encoding(&self) -> std::option::Option { return self.encoding; } /// Returns the highest transaction version the caller declares it can consume. #[must_use] pub const fn max_supported_transaction_version(&self) -> std::option::Option { return self.max_supported_transaction_version; } /// Returns whether the modern config would serialize to an empty object. #[cfg(test)] pub(crate) const fn is_empty(&self) -> bool { return self.commitment.is_none() && self.encoding.is_none() && self.max_supported_transaction_version.is_none(); } /// Serializes the modern config to its Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); } if let std::option::Option::Some(encoding) = self.encoding { object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned())); } if let std::option::Option::Some(version) = self.max_supported_transaction_version { object.insert("maxSupportedTransactionVersion".to_owned(), serde_json::Value::Number(version.into())); } return serde_json::Value::Object(object); } } /// Pagination and context configuration accepted by `getSignaturesForAddress`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SolanaSignaturesForAddressConfig { before: std::option::Option, until: std::option::Option, limit: std::option::Option, context: crate::SolanaContextConfig, } impl SolanaSignaturesForAddressConfig { /// Creates an address-signature pagination configuration. #[must_use] pub fn new( before: std::option::Option, until: std::option::Option, limit: std::option::Option, commitment: std::option::Option, min_context_slot: std::option::Option, ) -> Self { return Self { before, until, limit, context: crate::SolanaContextConfig::new(commitment, min_context_slot) }; } /// Returns the exclusive pagination signature preceding the requested page. #[must_use] pub fn before(&self) -> std::option::Option<&str> { return self.before.as_deref(); } /// Returns the exclusive pagination signature terminating the requested range. #[must_use] pub fn until(&self) -> std::option::Option<&str> { return self.until.as_deref(); } /// Returns the optional requested page size. #[must_use] pub const fn limit(&self) -> std::option::Option { return self.limit; } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.context.commitment(); } /// Returns the optional minimum context slot. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { return self.context.min_context_slot(); } /// Returns whether the pagination config would serialize to an empty object. pub(crate) fn is_empty(&self) -> bool { return self.before.is_none() && self.until.is_none() && self.limit.is_none() && self.commitment().is_none() && self.min_context_slot().is_none(); } /// Serializes this pagination config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(&self) -> serde_json::Value { let context = self.context.to_json_value(); let mut object = match context { serde_json::Value::Object(object) => object, _ => serde_json::Map::new(), }; if let std::option::Option::Some(before) = self.before.as_ref() { object.insert("before".to_owned(), serde_json::Value::String(before.clone())); } if let std::option::Option::Some(until) = self.until.as_ref() { object.insert("until".to_owned(), serde_json::Value::String(until.clone())); } if let std::option::Option::Some(limit) = self.limit { object.insert("limit".to_owned(), serde_json::json!(limit)); } return serde_json::Value::Object(object); } } /// Optional historical-search configuration accepted by `getSignatureStatuses`. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaSignatureStatusesConfig { search_transaction_history: std::option::Option, } impl SolanaSignatureStatusesConfig { /// Creates a signature-status configuration. #[must_use] pub const fn new(search_transaction_history: std::option::Option) -> Self { return Self { search_transaction_history }; } /// Returns whether full transaction history should be searched when explicitly configured. #[must_use] pub const fn search_transaction_history(&self) -> std::option::Option { return self.search_transaction_history; } /// Returns whether the status config would serialize to an empty object. pub(crate) const fn is_empty(&self) -> bool { return self.search_transaction_history.is_none(); } /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(value) = self.search_transaction_history { object.insert("searchTransactionHistory".to_owned(), serde_json::Value::Bool(value)); } return serde_json::Value::Object(object); } } /// Configuration accepted by `requestAirdrop`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SolanaRequestAirdropConfig { recent_blockhash: std::option::Option, commitment: std::option::Option, } impl SolanaRequestAirdropConfig { /// Creates an airdrop request configuration. #[must_use] pub fn new(recent_blockhash: std::option::Option, commitment: std::option::Option) -> Self { return Self { recent_blockhash, commitment }; } /// Returns the optional recent blockhash supplied to the faucet RPC. #[must_use] pub fn recent_blockhash(&self) -> std::option::Option<&str> { return self.recent_blockhash.as_deref(); } /// Returns the optional commitment level. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns whether the airdrop config would serialize to an empty object. pub(crate) fn is_empty(&self) -> bool { return self.recent_blockhash.is_none() && self.commitment.is_none(); } /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(blockhash) = self.recent_blockhash.as_ref() { object.insert("recentBlockhash".to_owned(), serde_json::Value::String(blockhash.clone())); } if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); } return serde_json::Value::Object(object); } } /// Configuration accepted by `sendTransaction` without changing KSP transport retry semantics. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct SolanaSendTransactionConfig { skip_preflight: std::option::Option, preflight_commitment: std::option::Option, encoding: std::option::Option, max_retries: std::option::Option, min_context_slot: std::option::Option, } impl SolanaSendTransactionConfig { /// Creates a serialized-transaction submission configuration. #[must_use] pub const fn new( skip_preflight: std::option::Option, preflight_commitment: std::option::Option, encoding: std::option::Option, max_retries: std::option::Option, min_context_slot: std::option::Option, ) -> Self { return Self { skip_preflight, preflight_commitment, encoding, max_retries, min_context_slot }; } /// Returns the optional preflight-skip flag. #[must_use] pub const fn skip_preflight(&self) -> std::option::Option { return self.skip_preflight; } /// Returns the optional preflight commitment. #[must_use] pub const fn preflight_commitment(&self) -> std::option::Option { return self.preflight_commitment; } /// Returns the optional serialized transaction encoding. #[must_use] pub const fn encoding(&self) -> std::option::Option { return self.encoding; } /// Returns the optional node-side retransmission limit. #[must_use] pub const fn max_retries(&self) -> std::option::Option { return self.max_retries; } /// Returns the optional minimum context slot. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { return self.min_context_slot; } /// Returns whether the send config would serialize to an empty object. pub(crate) const fn is_empty(&self) -> bool { return self.skip_preflight.is_none() && self.preflight_commitment.is_none() && self.encoding.is_none() && self.max_retries.is_none() && self.min_context_slot.is_none(); } /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(value) = self.skip_preflight { object.insert("skipPreflight".to_owned(), serde_json::Value::Bool(value)); } if let std::option::Option::Some(commitment) = self.preflight_commitment { object.insert("preflightCommitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); } if let std::option::Option::Some(encoding) = self.encoding { object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned())); } if let std::option::Option::Some(max_retries) = self.max_retries { object.insert("maxRetries".to_owned(), serde_json::json!(max_retries)); } if let std::option::Option::Some(min_context_slot) = self.min_context_slot { object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into())); } return serde_json::Value::Object(object); } } /// Account-return configuration nested under `simulateTransaction`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SolanaSimulationAccountsConfig { encoding: std::option::Option, addresses: std::vec::Vec, } impl SolanaSimulationAccountsConfig { /// Creates a simulation account-return configuration. #[must_use] pub fn new(encoding: std::option::Option, addresses: std::vec::Vec) -> Self { return Self { encoding, addresses }; } /// Returns the optional account-data encoding. #[must_use] pub const fn encoding(&self) -> std::option::Option { return self.encoding; } /// Returns the requested account addresses in caller order. #[must_use] pub fn addresses(&self) -> &[ksp_core_lib::Pubkey] { return self.addresses.as_slice(); } /// Serializes the nested account config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(encoding) = self.encoding { object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned())); } let mut addresses = std::vec::Vec::with_capacity(self.addresses.len()); for address in &self.addresses { addresses.push(serde_json::Value::String(address.to_string())); } object.insert("addresses".to_owned(), serde_json::Value::Array(addresses)); return serde_json::Value::Object(object); } } /// Configuration accepted by `simulateTransaction`. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SolanaSimulateTransactionConfig { commitment: std::option::Option, encoding: std::option::Option, replace_recent_blockhash: std::option::Option, sig_verify: std::option::Option, min_context_slot: std::option::Option, inner_instructions: std::option::Option, accounts: std::option::Option, } impl SolanaSimulateTransactionConfig { /// Creates a simulation configuration while leaving deterministic validation to the simulation wrapper. #[must_use] pub fn new( commitment: std::option::Option, encoding: std::option::Option, replace_recent_blockhash: std::option::Option, sig_verify: std::option::Option, min_context_slot: std::option::Option, inner_instructions: std::option::Option, accounts: std::option::Option, ) -> Self { return Self { commitment, encoding, replace_recent_blockhash, sig_verify, min_context_slot, inner_instructions, accounts, }; } /// Returns the optional simulation commitment. #[must_use] pub const fn commitment(&self) -> std::option::Option { return self.commitment; } /// Returns the optional serialized transaction encoding. #[must_use] pub const fn encoding(&self) -> std::option::Option { return self.encoding; } /// Returns the optional recent-blockhash replacement flag. #[must_use] pub const fn replace_recent_blockhash(&self) -> std::option::Option { return self.replace_recent_blockhash; } /// Returns the optional signature-verification flag. #[must_use] pub const fn sig_verify(&self) -> std::option::Option { return self.sig_verify; } /// Returns the optional minimum context slot. #[must_use] pub const fn min_context_slot(&self) -> std::option::Option { return self.min_context_slot; } /// Returns the optional inner-instruction recording flag. #[must_use] pub const fn inner_instructions(&self) -> std::option::Option { return self.inner_instructions; } /// Returns the optional post-simulation account-return configuration. #[must_use] pub const fn accounts(&self) -> std::option::Option<&crate::SolanaSimulationAccountsConfig> { return self.accounts.as_ref(); } /// Returns whether the simulation config would serialize to an empty object. pub(crate) fn is_empty(&self) -> bool { return self.commitment.is_none() && self.encoding.is_none() && self.replace_recent_blockhash.is_none() && self.sig_verify.is_none() && self.min_context_slot.is_none() && self.inner_instructions.is_none() && self.accounts.is_none(); } /// Serializes this config to the Solana JSON-RPC wire object. #[must_use] pub(crate) fn to_json_value(&self) -> serde_json::Value { let mut object = serde_json::Map::new(); if let std::option::Option::Some(commitment) = self.commitment { object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned())); } if let std::option::Option::Some(encoding) = self.encoding { object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned())); } if let std::option::Option::Some(value) = self.replace_recent_blockhash { object.insert("replaceRecentBlockhash".to_owned(), serde_json::Value::Bool(value)); } if let std::option::Option::Some(value) = self.sig_verify { object.insert("sigVerify".to_owned(), serde_json::Value::Bool(value)); } if let std::option::Option::Some(min_context_slot) = self.min_context_slot { object.insert("minContextSlot".to_owned(), serde_json::Value::Number(min_context_slot.into())); } if let std::option::Option::Some(value) = self.inner_instructions { object.insert("innerInstructions".to_owned(), serde_json::Value::Bool(value)); } if let std::option::Option::Some(accounts) = self.accounts.as_ref() { object.insert("accounts".to_owned(), accounts.to_json_value()); } return serde_json::Value::Object(object); } } /// Blockhash information returned by `getLatestBlockhash` and optionally by simulation. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SolanaLatestBlockhash { blockhash: std::string::String, last_valid_block_height: u64, } impl SolanaLatestBlockhash { /// Returns the base58 blockhash string without locally decoding it. #[must_use] pub fn blockhash(&self) -> &str { return self.blockhash.as_str(); } /// Returns the last valid block height for this blockhash. #[must_use] pub const fn last_valid_block_height(&self) -> u64 { return self.last_valid_block_height; } /// Decodes a latest-blockhash object from the Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if wire.blockhash.is_empty() { return std::result::Result::Err(invalid_transaction_wire(method, "blockhash", "latest blockhash must not be empty")); } return std::result::Result::Ok(Self { blockhash: wire.blockhash, last_valid_block_height: wire.last_valid_block_height }); } } /// One recent prioritization-fee sample returned by `getRecentPrioritizationFees`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct SolanaPrioritizationFee { slot: u64, prioritization_fee: u64, } impl SolanaPrioritizationFee { /// Returns the sample slot. #[must_use] pub const fn slot(&self) -> u64 { return self.slot; } /// Returns the minimum prioritization fee in micro-lamports reported for the sample. #[must_use] pub const fn prioritization_fee(&self) -> u64 { return self.prioritization_fee; } /// Decodes one prioritization-fee sample from the Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); return match decoded { std::result::Result::Ok(wire) => std::result::Result::Ok(Self { slot: wire.slot, prioritization_fee: wire.prioritization_fee }), std::result::Result::Err(error) => std::result::Result::Err(error), }; } } /// Confirmation state reported for a signature or transaction status. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SolanaTransactionConfirmationStatus { /// The transaction has been processed by a node. Processed, /// The transaction has reached confirmed commitment. Confirmed, /// The transaction has reached finalized commitment. Finalized, } /// One ordered signature record returned by `getSignaturesForAddress`. #[derive(Clone, Debug, PartialEq)] pub struct SolanaSignatureInfo { signature: std::string::String, slot: u64, err: std::option::Option, memo: std::option::Option, block_time: std::option::Option, confirmation_status: std::option::Option, transaction_index: std::option::Option, } impl SolanaSignatureInfo { /// Returns the base58 transaction signature string without locally decoding it. #[must_use] pub fn signature(&self) -> &str { return self.signature.as_str(); } /// Returns the transaction slot. #[must_use] pub const fn slot(&self) -> u64 { return self.slot; } /// Returns the nullable transaction error wire value. #[must_use] pub const fn err(&self) -> std::option::Option<&serde_json::Value> { return self.err.as_ref(); } /// Returns the nullable memo text. #[must_use] pub fn memo(&self) -> std::option::Option<&str> { return self.memo.as_deref(); } /// Returns the nullable block time as a Unix timestamp. #[must_use] pub const fn block_time(&self) -> std::option::Option { return self.block_time; } /// Returns the optional confirmation status. #[must_use] pub const fn confirmation_status(&self) -> std::option::Option { return self.confirmation_status; } /// Returns the optional transaction index exposed by current Agave runtimes. #[must_use] pub const fn transaction_index(&self) -> std::option::Option { return self.transaction_index; } /// Decodes one signature record from the Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let status = decode_confirmation_status(method, "confirmationStatus", wire.confirmation_status); let confirmation_status = match status { std::result::Result::Ok(status) => status, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { signature: wire.signature, slot: wire.slot, err: wire.err, memo: wire.memo, block_time: wire.block_time, confirmation_status, transaction_index: wire.transaction_index, }); } } /// One non-null position returned by `getSignatureStatuses`. #[derive(Clone, Debug, PartialEq)] pub struct SolanaSignatureStatus { slot: u64, confirmations: std::option::Option, status: serde_json::Value, err: std::option::Option, confirmation_status: std::option::Option, } impl SolanaSignatureStatus { /// Returns the slot at which the signature status was observed. #[must_use] pub const fn slot(&self) -> u64 { return self.slot; } /// Returns remaining confirmations, or `None` when the transaction is rooted. #[must_use] pub const fn confirmations(&self) -> std::option::Option { return self.confirmations; } /// Returns the legacy `status` result field losslessly. #[must_use] pub const fn status(&self) -> &serde_json::Value { return &self.status; } /// Returns the nullable transaction error wire value. #[must_use] pub const fn err(&self) -> std::option::Option<&serde_json::Value> { return self.err.as_ref(); } /// Returns the optional confirmation status. #[must_use] pub const fn confirmation_status(&self) -> std::option::Option { return self.confirmation_status; } /// Decodes one present signature status from the Solana JSON wire shape. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let status = decode_confirmation_status(method, "confirmationStatus", wire.confirmation_status); let confirmation_status = match status { std::result::Result::Ok(status) => status, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { slot: wire.slot, confirmations: wire.confirmations, status: wire.status, err: wire.err, confirmation_status, }); } } /// Wire-preserving transaction payload returned by `getTransaction`. #[derive(Clone, Debug, PartialEq)] pub enum SolanaEncodedTransaction { /// Legacy single-string base58 form retained for RPC backwards compatibility. LegacyBinary(std::string::String), /// Explicit encoded tuple `[data, "base58" | "base64"]`. Binary { /// Encoded transaction bytes. data: std::string::String, /// Explicit binary encoding label. encoding: crate::SolanaTransactionBinaryEncoding, }, /// JSON or `jsonParsed` object preserved without Program-specific decoding. Json(serde_json::Value), } impl SolanaEncodedTransaction { /// Decodes the untagged transaction wire union without decoding transaction bytes. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { return match value { serde_json::Value::String(data) => std::result::Result::Ok(Self::LegacyBinary(data)), serde_json::Value::Object(object) => std::result::Result::Ok(Self::Json(serde_json::Value::Object(object))), serde_json::Value::Array(values) => decode_binary_transaction_tuple(method, values), _ => std::result::Result::Err(invalid_transaction_wire(method, "transaction", "transaction payload has an unsupported JSON shape")), }; } } /// Transaction version reported by `getTransaction` when the version field is present. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum SolanaTransactionVersion { /// Legacy transaction without a versioned message prefix. Legacy, /// Numeric versioned-transaction identifier. Number(u8), } impl SolanaTransactionVersion { /// Decodes a transaction version from the shared Solana JSON wire representation. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { return match value { serde_json::Value::String(value) if value == "legacy" => std::result::Result::Ok(Self::Legacy), serde_json::Value::Number(value) => { let number = value.as_u64(); let number = match number { std::option::Option::Some(number) => number, std::option::Option::None => { return std::result::Result::Err(invalid_transaction_wire( method, "version", "transaction version must be an unsigned integer or legacy", )); }, }; let converted = u8::try_from(number); return match converted { std::result::Result::Ok(number) => std::result::Result::Ok(Self::Number(number)), std::result::Result::Err(_) => { std::result::Result::Err(invalid_transaction_wire(method, "version", "transaction version is outside the supported wire integer range")) }, }; }, _ => std::result::Result::Err(invalid_transaction_wire(method, "version", "transaction version must be an unsigned integer or legacy")), }; } } /// Confirmed transaction result returned by `getTransaction` when the RPC result is non-null. #[derive(Clone, Debug, PartialEq)] pub struct SolanaConfirmedTransaction { slot: u64, block_time: std::option::Option, transaction: crate::SolanaEncodedTransaction, meta: crate::SolanaWireField, version: crate::SolanaWireField, transaction_index: crate::SolanaWireField, } impl SolanaConfirmedTransaction { /// Returns the containing slot. #[must_use] pub const fn slot(&self) -> u64 { return self.slot; } /// Returns the nullable block time as a Unix timestamp. #[must_use] pub const fn block_time(&self) -> std::option::Option { return self.block_time; } /// Returns the wire-preserving encoded/JSON transaction payload. #[must_use] pub const fn transaction(&self) -> &crate::SolanaEncodedTransaction { return &self.transaction; } /// Returns the transaction metadata while preserving omitted/null/present states. #[must_use] pub const fn meta(&self) -> &crate::SolanaWireField { return &self.meta; } /// Returns the transaction version while preserving omitted/null/present states. #[must_use] pub const fn version(&self) -> &crate::SolanaWireField { return &self.version; } /// Returns the current optional transaction index while preserving omitted/null/present states. #[must_use] pub const fn transaction_index(&self) -> &crate::SolanaWireField { return &self.transaction_index; } /// Decodes one non-null confirmed transaction without decoding Program-specific transaction internals. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transaction = crate::SolanaEncodedTransaction::decode_wire(method, wire.transaction); let transaction = match transaction { std::result::Result::Ok(transaction) => transaction, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let version = decode_transaction_version_field(method, wire.version); let version = match version { std::result::Result::Ok(version) => version, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { slot: wire.slot, block_time: wire.block_time, transaction, meta: wire.meta, version, transaction_index: wire.transaction_index, }); } } /// Rich result payload returned inside the contextual `simulateTransaction` response. #[derive(Clone, Debug, PartialEq)] pub struct SolanaSimulateTransactionResult { err: crate::SolanaWireField, logs: crate::SolanaWireField>, accounts: crate::SolanaWireField>>, units_consumed: crate::SolanaWireField, loaded_accounts_data_size: crate::SolanaWireField, return_data: crate::SolanaWireField, inner_instructions: crate::SolanaWireField, replacement_blockhash: crate::SolanaWireField, fee: crate::SolanaWireField, pre_balances: crate::SolanaWireField>, post_balances: crate::SolanaWireField>, pre_token_balances: crate::SolanaWireField, post_token_balances: crate::SolanaWireField, loaded_addresses: crate::SolanaWireField, } impl SolanaSimulateTransactionResult { /// Returns the simulation error while preserving omission versus explicit `null`. #[must_use] pub const fn err(&self) -> &crate::SolanaWireField { return &self.err; } /// Returns simulation logs while preserving omission versus explicit `null`. #[must_use] pub const fn logs(&self) -> &crate::SolanaWireField> { return &self.logs; } /// Returns requested post-simulation accounts with positional nulls preserved. #[must_use] pub const fn accounts(&self) -> &crate::SolanaWireField>> { return &self.accounts; } /// Returns compute units consumed when the runtime reports them. #[must_use] pub const fn units_consumed(&self) -> &crate::SolanaWireField { return &self.units_consumed; } /// Returns loaded-account data size when the runtime reports it. #[must_use] pub const fn loaded_accounts_data_size(&self) -> &crate::SolanaWireField { return &self.loaded_accounts_data_size; } /// Returns Program return data losslessly at the transport boundary. #[must_use] pub const fn return_data(&self) -> &crate::SolanaWireField { return &self.return_data; } /// Returns inner instructions losslessly without Program-specific decoding. #[must_use] pub const fn inner_instructions(&self) -> &crate::SolanaWireField { return &self.inner_instructions; } /// Returns a replacement blockhash when the runtime produced one. #[must_use] pub const fn replacement_blockhash(&self) -> &crate::SolanaWireField { return &self.replacement_blockhash; } /// Returns the simulated fee when reported. #[must_use] pub const fn fee(&self) -> &crate::SolanaWireField { return &self.fee; } /// Returns pre-simulation lamport balances when reported. #[must_use] pub const fn pre_balances(&self) -> &crate::SolanaWireField> { return &self.pre_balances; } /// Returns post-simulation lamport balances when reported. #[must_use] pub const fn post_balances(&self) -> &crate::SolanaWireField> { return &self.post_balances; } /// Returns pre-simulation token balances losslessly without SPL decoding. #[must_use] pub const fn pre_token_balances(&self) -> &crate::SolanaWireField { return &self.pre_token_balances; } /// Returns post-simulation token balances losslessly without SPL decoding. #[must_use] pub const fn post_token_balances(&self) -> &crate::SolanaWireField { return &self.post_token_balances; } /// Returns loaded-address metadata losslessly without transaction-message decoding. #[must_use] pub const fn loaded_addresses(&self) -> &crate::SolanaWireField { return &self.loaded_addresses; } /// Decodes a simulation result while preserving optional/nullable current Agave fields. pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let accounts = decode_simulation_accounts_field(method, wire.accounts); let accounts = match accounts { std::result::Result::Ok(accounts) => accounts, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let replacement_blockhash = decode_replacement_blockhash_field(method, wire.replacement_blockhash); let replacement_blockhash = match replacement_blockhash { std::result::Result::Ok(blockhash) => blockhash, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { err: wire.err, logs: wire.logs, accounts, units_consumed: wire.units_consumed, loaded_accounts_data_size: wire.loaded_accounts_data_size, return_data: wire.return_data, inner_instructions: wire.inner_instructions, replacement_blockhash, fee: wire.fee, pre_balances: wire.pre_balances, post_balances: wire.post_balances, pre_token_balances: wire.pre_token_balances, post_token_balances: wire.post_token_balances, loaded_addresses: wire.loaded_addresses, }); } } impl crate::HttpTransportPool { /// Executes typed `getFeeForMessage` through the common KSP HTTP transport path. pub async fn get_fee_for_message( &self, role: &crate::HttpRoleName, message_base64: &str, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result>> { let mut params = std::vec![serde_json::Value::String(message_base64.to_owned())]; push_transaction_context_config(&mut params, config); let value = self.execute_transaction_rpc("getFeeForMessage", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let contextual = decode_transaction_contextual_wire("getFeeForMessage", value); let (context, value) = match contextual { std::result::Result::Ok(contextual) => contextual, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let fee = crate::decode_wire_json::>("getFeeForMessage", value); return match fee { std::result::Result::Ok(fee) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, fee)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getLatestBlockhash` through the common KSP HTTP transport path. pub async fn get_latest_blockhash( &self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result> { let mut params = std::vec::Vec::new(); push_transaction_context_config(&mut params, config); let value = self.execute_transaction_rpc("getLatestBlockhash", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let contextual = decode_transaction_contextual_wire("getLatestBlockhash", value); let (context, value) = match contextual { std::result::Result::Ok(contextual) => contextual, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let blockhash = crate::SolanaLatestBlockhash::decode_wire("getLatestBlockhash", value); return match blockhash { std::result::Result::Ok(blockhash) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, blockhash)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getRecentPrioritizationFees` through the common KSP HTTP transport path. pub async fn get_recent_prioritization_fees( &self, role: &crate::HttpRoleName, writable_accounts: std::option::Option<&[ksp_core_lib::Pubkey]>, ) -> ksp_core_lib::Result> { if let std::option::Option::Some(writable_accounts) = writable_accounts && writable_accounts.len() > MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS { return invalid_transaction_parameters( "getRecentPrioritizationFees", "getRecentPrioritizationFees accepts at most 128 account addresses", "account_count", writable_accounts.len(), ); } let mut params = std::vec::Vec::new(); if let std::option::Option::Some(writable_accounts) = writable_accounts { let mut addresses = std::vec::Vec::with_capacity(writable_accounts.len()); for account in writable_accounts { addresses.push(serde_json::Value::String(account.to_string())); } params.push(serde_json::Value::Array(addresses)); } let value = self.execute_transaction_rpc("getRecentPrioritizationFees", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_prioritization_fees("getRecentPrioritizationFees", value); } /// Executes typed `getSignaturesForAddress` through the common KSP HTTP transport path. pub async fn get_signatures_for_address( &self, role: &crate::HttpRoleName, address: &ksp_core_lib::Pubkey, config: std::option::Option<&crate::SolanaSignaturesForAddressConfig>, ) -> ksp_core_lib::Result> { if let std::option::Option::Some(limit) = config.and_then(crate::SolanaSignaturesForAddressConfig::limit) && (limit == 0 || limit > MAX_SIGNATURES_FOR_ADDRESS_LIMIT) { return invalid_transaction_parameters("getSignaturesForAddress", "getSignaturesForAddress limit must be between 1 and 1000", "limit", limit); } let mut params = std::vec![serde_json::Value::String(address.to_string())]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push(config.to_json_value()); } let value = self.execute_transaction_rpc("getSignaturesForAddress", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return decode_signature_infos("getSignaturesForAddress", value); } /// Executes typed `getSignatureStatuses` through the common KSP HTTP transport path. pub async fn get_signature_statuses( &self, role: &crate::HttpRoleName, signatures: &[std::string::String], config: std::option::Option<&crate::SolanaSignatureStatusesConfig>, ) -> ksp_core_lib::Result>>> { if signatures.len() > MAX_SIGNATURE_STATUSES { return invalid_transaction_parameters( "getSignatureStatuses", "getSignatureStatuses accepts at most 256 signatures", "signature_count", signatures.len(), ); } let mut signature_values = std::vec::Vec::with_capacity(signatures.len()); for signature in signatures { signature_values.push(serde_json::Value::String(signature.clone())); } let mut params = std::vec![serde_json::Value::Array(signature_values)]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push((*config).to_json_value()); } let value = self.execute_transaction_rpc("getSignatureStatuses", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let contextual = decode_transaction_contextual_wire("getSignatureStatuses", value); let (context, value) = match contextual { std::result::Result::Ok(contextual) => contextual, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let statuses = decode_signature_statuses("getSignatureStatuses", value, signatures.len()); return match statuses { std::result::Result::Ok(statuses) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, statuses)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes the current object-form `getTransaction` request through the common KSP HTTP transport path. /// /// `None` omits the optional second parameter. `Some(config)` sends the modern object form exactly, including an empty `{}` object when the /// caller explicitly supplies an empty modern configuration object. pub async fn get_transaction( &self, role: &crate::HttpRoleName, signature: &str, config: std::option::Option<&crate::SolanaGetTransactionConfig>, ) -> ksp_core_lib::Result> { if let std::option::Option::Some(config) = config && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "getTransaction commitment must be confirmed or finalized when explicitly provided", ) .with_context("rpc_method", "getTransaction") .with_context("commitment", "processed"), ); } let mut params = std::vec![serde_json::Value::String(signature.to_owned())]; if let std::option::Option::Some(config) = config { params.push((*config).to_json_value()); } return self.execute_get_transaction(role, params).await; } /// Executes the deprecated bare-encoding `getTransaction` request form retained by Solana RPC for backwards compatibility. #[deprecated(note = "use HttpTransportPool::get_transaction with SolanaGetTransactionConfig; the bare encoding request form is deprecated")] pub async fn get_transaction_legacy( &self, role: &crate::HttpRoleName, signature: &str, encoding: crate::SolanaTransactionEncoding, ) -> ksp_core_lib::Result> { ksp_logging_lib::warn!( target: crate::TRACING_TARGET, rpc_method = "getTransaction", request_form = "bare_encoding", encoding = encoding.as_str(), "deprecated Solana HTTP RPC request form used" ); let params = std::vec![serde_json::Value::String(signature.to_owned()), serde_json::Value::String(encoding.as_str().to_owned()),]; return self.execute_get_transaction(role, params).await; } async fn execute_get_transaction( &self, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result> { let value = self.execute_transaction_rpc("getTransaction", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if value.is_null() { return std::result::Result::Ok(std::option::Option::None); } let transaction = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", value); return match transaction { std::result::Result::Ok(transaction) => std::result::Result::Ok(std::option::Option::Some(transaction)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `requestAirdrop` through the common KSP HTTP transport path. /// /// The RPC creates and submits a faucet transaction, so its audited descriptor is `WriteSubmission / NeverAfterDispatch`. The optional /// `recentBlockhash` field is retained from the targeted Agave runtime even though the public Solana page currently documents only `commitment`. pub async fn request_airdrop( &self, role: &crate::HttpRoleName, recipient: &ksp_core_lib::Pubkey, lamports: u64, config: std::option::Option<&crate::SolanaRequestAirdropConfig>, ) -> ksp_core_lib::Result { let mut params = std::vec![serde_json::Value::String(recipient.to_string()), serde_json::json!(lamports)]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push(config.to_json_value()); } let value = self.execute_transaction_rpc("requestAirdrop", role, params).await; return match value { std::result::Result::Ok(value) => crate::decode_wire_json::("requestAirdrop", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `sendTransaction` through the common KSP HTTP transport path. /// /// Transport forwards an already serialized and signed transaction without decoding or modifying it. `config.maxRetries` controls node-side /// retransmission only; KSP's HTTP retry policy remains governed by the central `WriteSubmission / NeverAfterDispatch` descriptor. pub async fn send_transaction( &self, role: &crate::HttpRoleName, transaction: &str, config: std::option::Option<&crate::SolanaSendTransactionConfig>, ) -> ksp_core_lib::Result { let mut params = std::vec![serde_json::Value::String(transaction.to_owned())]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push((*config).to_json_value()); } let value = self.execute_transaction_rpc("sendTransaction", role, params).await; return match value { std::result::Result::Ok(value) => crate::decode_wire_json::("sendTransaction", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `simulateTransaction` through the common KSP HTTP transport path. /// /// Transport forwards the already encoded transaction as opaque text. The wrapper enforces only deterministic RPC invariants that do not require /// decoding transaction bytes: `sigVerify` cannot be combined with `replaceRecentBlockhash`, and simulation account-return encoding cannot use the /// legacy `binary` / `base58` account encodings rejected by the targeted Agave runtime. pub async fn simulate_transaction( &self, role: &crate::HttpRoleName, transaction: &str, config: std::option::Option<&crate::SolanaSimulateTransactionConfig>, ) -> ksp_core_lib::Result> { if let std::option::Option::Some(config) = config { if config.sig_verify() == std::option::Option::Some(true) && config.replace_recent_blockhash() == std::option::Option::Some(true) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "simulateTransaction sigVerify may not be used with replaceRecentBlockhash", ) .with_context("rpc_method", "simulateTransaction"), ); } if let std::option::Option::Some(accounts) = config.accounts() && let std::option::Option::Some(encoding) = accounts.encoding() && (encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::Base58) { return std::result::Result::Err( ksp_core_lib::Error::new( crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "simulateTransaction account-return encoding must be base64, base64+zstd, or jsonParsed", ) .with_context("rpc_method", "simulateTransaction") .with_context("accounts_encoding", encoding.as_str()), ); } } let mut params = std::vec![serde_json::Value::String(transaction.to_owned())]; if let std::option::Option::Some(config) = config && !config.is_empty() { params.push(config.to_json_value()); } let value = self.execute_transaction_rpc("simulateTransaction", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let contextual = decode_transaction_contextual_wire("simulateTransaction", value); let (context, value) = match contextual { std::result::Result::Ok(contextual) => contextual, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let result = crate::SolanaSimulateTransactionResult::decode_wire("simulateTransaction", value); return match result { std::result::Result::Ok(result) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, result)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `getTransactionCount` through the common KSP HTTP transport path. pub async fn get_transaction_count( &self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result { let mut params = std::vec::Vec::new(); push_transaction_context_config(&mut params, config); let value = self.execute_transaction_rpc("getTransactionCount", role, params).await; return match value { std::result::Result::Ok(value) => crate::decode_wire_json::("getTransactionCount", value), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Executes typed `isBlockhashValid` through the common KSP HTTP transport path. pub async fn is_blockhash_valid( &self, role: &crate::HttpRoleName, blockhash: &str, config: std::option::Option<&crate::SolanaContextConfig>, ) -> ksp_core_lib::Result> { let mut params = std::vec![serde_json::Value::String(blockhash.to_owned())]; push_transaction_context_config(&mut params, config); let value = self.execute_transaction_rpc("isBlockhashValid", role, params).await; let value = match value { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let contextual = decode_transaction_contextual_wire("isBlockhashValid", value); let (context, value) = match contextual { std::result::Result::Ok(contextual) => contextual, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let valid = crate::decode_wire_json::("isBlockhashValid", value); return match valid { std::result::Result::Ok(valid) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, valid)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } async fn execute_transaction_rpc( &self, method_name: &'static str, role: &crate::HttpRoleName, params: std::vec::Vec, ) -> ksp_core_lib::Result { let method = transaction_descriptor(method_name); let method = match method { std::result::Result::Ok(method) => method, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return self.execute_standard_rpc(role, method, params).await; } } #[derive(serde::Deserialize)] struct WireTransactionRpcResponse { context: serde_json::Value, value: serde_json::Value, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireLatestBlockhash { blockhash: std::string::String, last_valid_block_height: u64, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WirePrioritizationFee { slot: u64, prioritization_fee: u64, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireSignatureInfo { signature: std::string::String, slot: u64, #[serde(default)] err: std::option::Option, #[serde(default)] memo: std::option::Option, #[serde(default)] block_time: std::option::Option, #[serde(default)] confirmation_status: std::option::Option, #[serde(default)] transaction_index: std::option::Option, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireSignatureStatus { slot: u64, #[serde(default)] confirmations: std::option::Option, status: serde_json::Value, #[serde(default)] err: std::option::Option, #[serde(default)] confirmation_status: std::option::Option, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireConfirmedTransaction { slot: u64, transaction: serde_json::Value, #[serde(default)] meta: crate::SolanaWireField, #[serde(default)] block_time: std::option::Option, #[serde(default)] version: crate::SolanaWireField, #[serde(default)] transaction_index: crate::SolanaWireField, } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireSimulateTransactionResult { #[serde(default)] err: crate::SolanaWireField, #[serde(default)] logs: crate::SolanaWireField>, #[serde(default)] accounts: crate::SolanaWireField>>, #[serde(default)] units_consumed: crate::SolanaWireField, #[serde(default)] loaded_accounts_data_size: crate::SolanaWireField, #[serde(default)] return_data: crate::SolanaWireField, #[serde(default)] inner_instructions: crate::SolanaWireField, #[serde(default)] replacement_blockhash: crate::SolanaWireField, #[serde(default)] fee: crate::SolanaWireField, #[serde(default)] pre_balances: crate::SolanaWireField>, #[serde(default)] post_balances: crate::SolanaWireField>, #[serde(default)] pre_token_balances: crate::SolanaWireField, #[serde(default)] post_token_balances: crate::SolanaWireField, #[serde(default)] loaded_addresses: crate::SolanaWireField, } fn push_transaction_context_config(params: &mut std::vec::Vec, config: std::option::Option<&crate::SolanaContextConfig>) { if let std::option::Option::Some(config) = config && (config.commitment().is_some() || config.min_context_slot().is_some()) { params.push((*config).to_json_value()); } return; } fn decode_transaction_contextual_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<(crate::SolanaRpcContext, serde_json::Value)> { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { std::result::Result::Ok(wire) => wire, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let context = crate::SolanaRpcContext::decode_wire(method, wire.context); return match context { std::result::Result::Ok(context) => std::result::Result::Ok((context, wire.value)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } fn decode_prioritization_fees(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let decoded = crate::decode_wire_json::>(method, value); let values = match decoded { std::result::Result::Ok(values) => values, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut fees = std::vec::Vec::with_capacity(values.len()); for value in values { let fee = crate::SolanaPrioritizationFee::decode_wire(method, value); match fee { std::result::Result::Ok(fee) => fees.push(fee), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(fees); } fn decode_signature_infos(method: &str, value: serde_json::Value) -> ksp_core_lib::Result> { let decoded = crate::decode_wire_json::>(method, value); let values = match decoded { std::result::Result::Ok(values) => values, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut infos = std::vec::Vec::with_capacity(values.len()); for value in values { let info = crate::SolanaSignatureInfo::decode_wire(method, value); match info { std::result::Result::Ok(info) => infos.push(info), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(infos); } fn decode_signature_statuses( method: &str, value: serde_json::Value, expected_count: usize, ) -> ksp_core_lib::Result>> { let decoded = crate::decode_wire_json::>>(method, value); let values = match decoded { std::result::Result::Ok(values) => values, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if values.len() != expected_count { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "getSignatureStatuses result count does not match the requested signature count") .with_context("rpc_method", method) .with_context("expected_count", expected_count.to_string()) .with_context("actual_count", values.len().to_string()), ); } let mut statuses = std::vec::Vec::with_capacity(values.len()); for value in values { let value = match value { std::option::Option::Some(value) => value, std::option::Option::None => { statuses.push(std::option::Option::None); continue; }, }; let status = crate::SolanaSignatureStatus::decode_wire(method, value); match status { std::result::Result::Ok(status) => statuses.push(std::option::Option::Some(status)), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(statuses); } fn invalid_transaction_parameters(method: &str, message: &str, field: &'static str, value: usize) -> ksp_core_lib::Result { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message) .with_context("rpc_method", method) .with_context(field, value.to_string()), ); } fn transaction_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { let descriptor = crate::find_http_rpc_method(method); return match descriptor { std::option::Option::Some(descriptor) if descriptor.category() == crate::HttpRpcCategory::Transactions && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_3 => { std::result::Result::Ok(descriptor) }, _ => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Transaction descriptor is missing or misclassified in the audited registry") .with_context("rpc_method", method), ), }; } fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field); } fn decode_confirmation_status( method: &str, field: &str, value: std::option::Option, ) -> ksp_core_lib::Result> { let value = match value { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), }; let status = match value.as_str() { "processed" => crate::SolanaTransactionConfirmationStatus::Processed, "confirmed" => crate::SolanaTransactionConfirmationStatus::Confirmed, "finalized" => crate::SolanaTransactionConfirmationStatus::Finalized, _ => return std::result::Result::Err(invalid_transaction_wire(method, field, "transaction confirmation status is unknown")), }; return std::result::Result::Ok(std::option::Option::Some(status)); } fn decode_binary_transaction_tuple(method: &str, values: std::vec::Vec) -> ksp_core_lib::Result { if values.len() != 2 { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple must contain data and encoding")); } let mut values = values.into_iter(); let data_value = match values.next() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple is missing data")); }, }; let encoding_value = match values.next() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple is missing encoding")); }, }; let data = match data_value.as_str() { std::option::Option::Some(value) => value.to_owned(), std::option::Option::None => { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple data must be a string")); }, }; let encoding = match encoding_value.as_str() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple encoding must be a string")); }, }; let encoding = crate::SolanaTransactionBinaryEncoding::from_wire(encoding); let encoding = match encoding { std::option::Option::Some(encoding) => encoding, std::option::Option::None => { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple uses an unsupported binary encoding")); }, }; return std::result::Result::Ok(crate::SolanaEncodedTransaction::Binary { data, encoding }); } fn decode_transaction_version_field( method: &str, field: crate::SolanaWireField, ) -> ksp_core_lib::Result> { return match field { crate::SolanaWireField::Omitted => std::result::Result::Ok(crate::SolanaWireField::Omitted), crate::SolanaWireField::Null => std::result::Result::Ok(crate::SolanaWireField::Null), crate::SolanaWireField::Value(value) => { let version = crate::SolanaTransactionVersion::decode_wire(method, value); match version { std::result::Result::Ok(version) => std::result::Result::Ok(crate::SolanaWireField::Value(version)), std::result::Result::Err(error) => std::result::Result::Err(error), } }, }; } fn decode_simulation_accounts_field( method: &str, field: crate::SolanaWireField>>, ) -> ksp_core_lib::Result>>> { let values = match field { crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted), crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null), crate::SolanaWireField::Value(values) => values, }; let mut accounts = std::vec::Vec::with_capacity(values.len()); for value in values { let value = match value { std::option::Option::Some(value) => value, std::option::Option::None => { accounts.push(std::option::Option::None); continue; }, }; let account = crate::SolanaAccount::decode_wire(method, value); match account { std::result::Result::Ok(account) => accounts.push(std::option::Option::Some(account)), std::result::Result::Err(error) => return std::result::Result::Err(error), } } return std::result::Result::Ok(crate::SolanaWireField::Value(accounts)); } fn decode_replacement_blockhash_field( method: &str, field: crate::SolanaWireField, ) -> ksp_core_lib::Result> { let value = match field { crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted), crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null), crate::SolanaWireField::Value(value) => value, }; let blockhash = crate::SolanaLatestBlockhash::decode_wire(method, value); return match blockhash { std::result::Result::Ok(blockhash) => std::result::Result::Ok(crate::SolanaWireField::Value(blockhash)), std::result::Result::Err(error) => std::result::Result::Err(error), }; } #[cfg(test)] #[path = "../unit_tests/rpc_transactions.rs"] mod tests;