v0.2.3-pre.004

This commit is contained in:
2026-08-18 12:14:59 +02:00
parent 2a7f3e8f40
commit bd26d4f636
14 changed files with 801 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 13
// version: 14
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -10,9 +10,10 @@
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and its first four read wrappers:
//! `getFeeForMessage`, `getLatestBlockhash`, `getTransactionCount` and `isBlockhashValid`. The remaining seven Transaction wrappers and the `0.2.4` family
//! remain staged.
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and seven read wrappers through `pre.004`,
//! including bounded prioritization-fee, address-signature and signature-status queries. `getTransaction`, both write submissions and
//! `simulateTransaction`, plus the
//! `0.2.4` family remain staged.
mod client;
mod constants;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 3
// version: 4
/// Binary encoding accepted for serialized transaction input payloads.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -236,14 +236,12 @@ impl SolanaSignaturesForAddressConfig {
}
/// Returns whether the pagination config would serialize to an empty object.
#[cfg(test)]
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]
#[cfg(test)]
pub(crate) fn to_json_value(&self) -> serde_json::Value {
let context = self.context.to_json_value();
let mut object = match context {
@@ -283,14 +281,12 @@ impl SolanaSignatureStatusesConfig {
}
/// Returns whether the status config would serialize to an empty object.
#[cfg(test)]
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]
#[cfg(test)]
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 {
@@ -651,7 +647,6 @@ impl SolanaPrioritizationFee {
}
/// Decodes one prioritization-fee sample from the Solana JSON wire shape.
#[cfg(test)]
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 {
@@ -728,7 +723,6 @@ impl SolanaSignatureInfo {
}
/// Decodes one signature record from the Solana JSON wire shape.
#[cfg(test)]
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 {
@@ -794,7 +788,6 @@ impl SolanaSignatureStatus {
}
/// Decodes one present signature status from the Solana JSON wire shape.
#[cfg(test)]
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 {
@@ -1102,6 +1095,10 @@ impl SolanaSimulateTransactionResult {
}
}
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(
@@ -1154,6 +1151,106 @@ impl crate::HttpTransportPool {
};
}
/// 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 typed `getTransactionCount` through the common KSP HTTP transport path.
pub async fn get_transaction_count(
&self,
@@ -1232,6 +1329,84 @@ fn decode_transaction_contextual_wire(method: &str, value: serde_json::Value) ->
};
}
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 {
@@ -1257,7 +1432,6 @@ fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) ->
return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field);
}
#[cfg(test)]
fn decode_confirmation_status(
method: &str,
field: &str,
@@ -1386,7 +1560,6 @@ struct WireLatestBlockhash {
last_valid_block_height: u64,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePrioritizationFee {
@@ -1394,7 +1567,6 @@ struct WirePrioritizationFee {
prioritization_fee: u64,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureInfo {
@@ -1412,7 +1584,6 @@ struct WireSignatureInfo {
transaction_index: std::option::Option<u32>,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureStatus {