Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
2026-08-18 16:12:07 +02:00

1808 lines
76 KiB
Rust

// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 7
/// 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<Self> {
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<T> {
/// 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<T> SolanaWireField<T> {
/// 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<T> std::default::Default for SolanaWireField<T> {
fn default() -> Self {
return Self::Omitted;
}
}
impl<'de, T> serde::Deserialize<'de> for SolanaWireField<T>
where
T: serde::Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let decoded = <std::option::Option<T> 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<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionEncoding>,
max_supported_transaction_version: std::option::Option<u8>,
}
impl SolanaGetTransactionConfig {
/// Creates a modern `getTransaction` configuration.
#[must_use]
pub const fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionEncoding>,
max_supported_transaction_version: std::option::Option<u8>,
) -> Self {
return Self { commitment, encoding, max_supported_transaction_version };
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional transaction response encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaTransactionEncoding> {
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<u8> {
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<std::string::String>,
until: std::option::Option<std::string::String>,
limit: std::option::Option<usize>,
context: crate::SolanaContextConfig,
}
impl SolanaSignaturesForAddressConfig {
/// Creates an address-signature pagination configuration.
#[must_use]
pub fn new(
before: std::option::Option<std::string::String>,
until: std::option::Option<std::string::String>,
limit: std::option::Option<usize>,
commitment: std::option::Option<crate::SolanaCommitment>,
min_context_slot: std::option::Option<u64>,
) -> 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<usize> {
return self.limit;
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.context.commitment();
}
/// Returns the optional minimum context slot.
#[must_use]
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
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<bool>,
}
impl SolanaSignatureStatusesConfig {
/// Creates a signature-status configuration.
#[must_use]
pub const fn new(search_transaction_history: std::option::Option<bool>) -> 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<bool> {
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<std::string::String>,
commitment: std::option::Option<crate::SolanaCommitment>,
}
impl SolanaRequestAirdropConfig {
/// Creates an airdrop request configuration.
#[must_use]
pub fn new(recent_blockhash: std::option::Option<std::string::String>, commitment: std::option::Option<crate::SolanaCommitment>) -> 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<crate::SolanaCommitment> {
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<bool>,
preflight_commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionBinaryEncoding>,
max_retries: std::option::Option<usize>,
min_context_slot: std::option::Option<u64>,
}
impl SolanaSendTransactionConfig {
/// Creates a serialized-transaction submission configuration.
#[must_use]
pub const fn new(
skip_preflight: std::option::Option<bool>,
preflight_commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionBinaryEncoding>,
max_retries: std::option::Option<usize>,
min_context_slot: std::option::Option<u64>,
) -> 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<bool> {
return self.skip_preflight;
}
/// Returns the optional preflight commitment.
#[must_use]
pub const fn preflight_commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.preflight_commitment;
}
/// Returns the optional serialized transaction encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaTransactionBinaryEncoding> {
return self.encoding;
}
/// Returns the optional node-side retransmission limit.
#[must_use]
pub const fn max_retries(&self) -> std::option::Option<usize> {
return self.max_retries;
}
/// Returns the optional minimum context slot.
#[must_use]
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
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<crate::SolanaAccountEncoding>,
addresses: std::vec::Vec<ksp_core_lib::Pubkey>,
}
impl SolanaSimulationAccountsConfig {
/// Creates a simulation account-return configuration.
#[must_use]
pub fn new(encoding: std::option::Option<crate::SolanaAccountEncoding>, addresses: std::vec::Vec<ksp_core_lib::Pubkey>) -> Self {
return Self { encoding, addresses };
}
/// Returns the optional account-data encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaAccountEncoding> {
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<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionBinaryEncoding>,
replace_recent_blockhash: std::option::Option<bool>,
sig_verify: std::option::Option<bool>,
min_context_slot: std::option::Option<u64>,
inner_instructions: std::option::Option<bool>,
accounts: std::option::Option<crate::SolanaSimulationAccountsConfig>,
}
impl SolanaSimulateTransactionConfig {
/// Creates a simulation configuration while leaving deterministic validation to the simulation wrapper.
#[must_use]
pub fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionBinaryEncoding>,
replace_recent_blockhash: std::option::Option<bool>,
sig_verify: std::option::Option<bool>,
min_context_slot: std::option::Option<u64>,
inner_instructions: std::option::Option<bool>,
accounts: std::option::Option<crate::SolanaSimulationAccountsConfig>,
) -> 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<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional serialized transaction encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaTransactionBinaryEncoding> {
return self.encoding;
}
/// Returns the optional recent-blockhash replacement flag.
#[must_use]
pub const fn replace_recent_blockhash(&self) -> std::option::Option<bool> {
return self.replace_recent_blockhash;
}
/// Returns the optional signature-verification flag.
#[must_use]
pub const fn sig_verify(&self) -> std::option::Option<bool> {
return self.sig_verify;
}
/// Returns the optional minimum context slot.
#[must_use]
pub const fn min_context_slot(&self) -> std::option::Option<u64> {
return self.min_context_slot;
}
/// Returns the optional inner-instruction recording flag.
#[must_use]
pub const fn inner_instructions(&self) -> std::option::Option<bool> {
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<Self> {
let decoded = crate::decode_wire_json::<WireLatestBlockhash>(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<Self> {
let decoded = crate::decode_wire_json::<WirePrioritizationFee>(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<serde_json::Value>,
memo: std::option::Option<std::string::String>,
block_time: std::option::Option<i64>,
confirmation_status: std::option::Option<crate::SolanaTransactionConfirmationStatus>,
transaction_index: std::option::Option<u32>,
}
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<i64> {
return self.block_time;
}
/// Returns the optional confirmation status.
#[must_use]
pub const fn confirmation_status(&self) -> std::option::Option<crate::SolanaTransactionConfirmationStatus> {
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<u32> {
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<Self> {
let decoded = crate::decode_wire_json::<WireSignatureInfo>(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<u64>,
status: serde_json::Value,
err: std::option::Option<serde_json::Value>,
confirmation_status: std::option::Option<crate::SolanaTransactionConfirmationStatus>,
}
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<u64> {
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<crate::SolanaTransactionConfirmationStatus> {
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<Self> {
let decoded = crate::decode_wire_json::<WireSignatureStatus>(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<Self> {
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 {
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
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<i64>,
transaction: crate::SolanaEncodedTransaction,
meta: crate::SolanaWireField<serde_json::Value>,
version: crate::SolanaWireField<crate::SolanaTransactionVersion>,
transaction_index: crate::SolanaWireField<u32>,
}
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<i64> {
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<serde_json::Value> {
return &self.meta;
}
/// Returns the transaction version while preserving omitted/null/present states.
#[must_use]
pub const fn version(&self) -> &crate::SolanaWireField<crate::SolanaTransactionVersion> {
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<u32> {
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<Self> {
let decoded = crate::decode_wire_json::<WireConfirmedTransaction>(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<serde_json::Value>,
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<crate::SolanaAccount>>>,
units_consumed: crate::SolanaWireField<u64>,
loaded_accounts_data_size: crate::SolanaWireField<u32>,
return_data: crate::SolanaWireField<serde_json::Value>,
inner_instructions: crate::SolanaWireField<serde_json::Value>,
replacement_blockhash: crate::SolanaWireField<crate::SolanaLatestBlockhash>,
fee: crate::SolanaWireField<u64>,
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
post_token_balances: crate::SolanaWireField<serde_json::Value>,
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
}
impl SolanaSimulateTransactionResult {
/// Returns the simulation error while preserving omission versus explicit `null`.
#[must_use]
pub const fn err(&self) -> &crate::SolanaWireField<serde_json::Value> {
return &self.err;
}
/// Returns simulation logs while preserving omission versus explicit `null`.
#[must_use]
pub const fn logs(&self) -> &crate::SolanaWireField<std::vec::Vec<std::string::String>> {
return &self.logs;
}
/// Returns requested post-simulation accounts with positional nulls preserved.
#[must_use]
pub const fn accounts(&self) -> &crate::SolanaWireField<std::vec::Vec<std::option::Option<crate::SolanaAccount>>> {
return &self.accounts;
}
/// Returns compute units consumed when the runtime reports them.
#[must_use]
pub const fn units_consumed(&self) -> &crate::SolanaWireField<u64> {
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<u32> {
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<serde_json::Value> {
return &self.return_data;
}
/// Returns inner instructions losslessly without Program-specific decoding.
#[must_use]
pub const fn inner_instructions(&self) -> &crate::SolanaWireField<serde_json::Value> {
return &self.inner_instructions;
}
/// Returns a replacement blockhash when the runtime produced one.
#[must_use]
pub const fn replacement_blockhash(&self) -> &crate::SolanaWireField<crate::SolanaLatestBlockhash> {
return &self.replacement_blockhash;
}
/// Returns the simulated fee when reported.
#[must_use]
pub const fn fee(&self) -> &crate::SolanaWireField<u64> {
return &self.fee;
}
/// Returns pre-simulation lamport balances when reported.
#[must_use]
pub const fn pre_balances(&self) -> &crate::SolanaWireField<std::vec::Vec<u64>> {
return &self.pre_balances;
}
/// Returns post-simulation lamport balances when reported.
#[must_use]
pub const fn post_balances(&self) -> &crate::SolanaWireField<std::vec::Vec<u64>> {
return &self.post_balances;
}
/// Returns pre-simulation token balances losslessly without SPL decoding.
#[must_use]
pub const fn pre_token_balances(&self) -> &crate::SolanaWireField<serde_json::Value> {
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<serde_json::Value> {
return &self.post_token_balances;
}
/// Returns loaded-address metadata losslessly without transaction-message decoding.
#[must_use]
pub const fn loaded_addresses(&self) -> &crate::SolanaWireField<serde_json::Value> {
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<Self> {
let decoded = crate::decode_wire_json::<WireSimulateTransactionResult>(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,
});
}
}
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
const MAX_SIGNATURE_STATUSES: usize = 256;
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<crate::SolanaRpcResponse<std::option::Option<u64>>> {
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::<std::option::Option<u64>>("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<crate::SolanaRpcResponse<crate::SolanaLatestBlockhash>> {
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<std::vec::Vec<crate::SolanaPrioritizationFee>> {
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<std::vec::Vec<crate::SolanaSignatureInfo>> {
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<crate::SolanaRpcResponse<std::vec::Vec<std::option::Option<crate::SolanaSignatureStatus>>>> {
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<std::option::Option<crate::SolanaConfirmedTransaction>> {
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<std::option::Option<crate::SolanaConfirmedTransaction>> {
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<serde_json::Value>,
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedTransaction>> {
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<std::string::String> {
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::<std::string::String>("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<std::string::String> {
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::<std::string::String>("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<crate::SolanaRpcResponse<crate::SolanaSimulateTransactionResult>> {
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<u64> {
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::<u64>("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<crate::SolanaRpcResponse<bool>> {
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::<bool>("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<serde_json::Value>,
) -> ksp_core_lib::Result<serde_json::Value> {
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;
}
}
fn push_transaction_context_config(params: &mut std::vec::Vec<serde_json::Value>, 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::<WireTransactionRpcResponse>(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<std::vec::Vec<crate::SolanaPrioritizationFee>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<serde_json::Value>>(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<std::vec::Vec<crate::SolanaSignatureInfo>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<serde_json::Value>>(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<std::vec::Vec<std::option::Option<crate::SolanaSignatureStatus>>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::option::Option<serde_json::Value>>>(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<T>(method: &str, message: &str, field: &'static str, value: usize) -> ksp_core_lib::Result<T> {
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),
),
};
}
#[derive(serde::Deserialize)]
struct WireTransactionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
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<std::string::String>,
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaTransactionConfirmationStatus>> {
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<serde_json::Value>) -> ksp_core_lib::Result<crate::SolanaEncodedTransaction> {
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<serde_json::Value>,
) -> ksp_core_lib::Result<crate::SolanaWireField<crate::SolanaTransactionVersion>> {
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<std::vec::Vec<std::option::Option<serde_json::Value>>>,
) -> ksp_core_lib::Result<crate::SolanaWireField<std::vec::Vec<std::option::Option<crate::SolanaAccount>>>> {
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<serde_json::Value>,
) -> ksp_core_lib::Result<crate::SolanaWireField<crate::SolanaLatestBlockhash>> {
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),
};
}
#[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_json::Value>,
#[serde(default)]
memo: std::option::Option<std::string::String>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
#[serde(default)]
transaction_index: std::option::Option<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureStatus {
slot: u64,
#[serde(default)]
confirmations: std::option::Option<u64>,
status: serde_json::Value,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedTransaction {
slot: u64,
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
transaction_index: crate::SolanaWireField<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSimulateTransactionResult {
#[serde(default)]
err: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
#[serde(default)]
units_consumed: crate::SolanaWireField<u64>,
#[serde(default)]
loaded_accounts_data_size: crate::SolanaWireField<u32>,
#[serde(default)]
return_data: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
inner_instructions: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
fee: crate::SolanaWireField<u64>,
#[serde(default)]
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
post_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_transactions.rs"]
mod tests;