v0.2.2-pre.006
This commit is contained in:
@@ -6,7 +6,7 @@ resolver = "3"
|
|||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.2-pre.5"
|
version = "0.2.2-pre.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":null,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"11111111111111111111111111111111":[0,2,4],"ComputeBudget111111111111111111111111111111":[1,3]} ,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":430000020,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"not-a-pubkey","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"ComputeBudget111111111111111111111111111111","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":["11111111111111111111111111111111","not-a-pubkey"],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":["11111111111111111111111111111111","ComputeBudget111111111111111111111111111111"],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"current":[{"votePubkey":"not-a-pubkey","nodePubkey":"11111111111111111111111111111111","activatedStake":1,"commission":5,"epochVoteAccount":true,"epochCredits":[],"lastVote":2,"rootSlot":1}],"delinquent":[]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"current":[{"votePubkey":"11111111111111111111111111111111","nodePubkey":"ComputeBudget111111111111111111111111111111","activatedStake":424242,"commission":8,"inflationRewardsCommissionBps":750,"epochVoteAccount":true,"epochCredits":[[700,100,90],[701,115,100]],"lastVote":999,"rootSlot":990}],"delinquent":[{"votePubkey":"ComputeBudget111111111111111111111111111111","nodePubkey":"11111111111111111111111111111111","activatedStake":1,"commission":5,"epochVoteAccount":false,"epochCredits":[],"lastVote":0,"rootSlot":0}]},"id":1}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 10
|
// version: 11
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
//! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains
|
//! This crate owns runtime HTTP transport settings, Solana HTTP JSON-RPC envelopes and the audited standard method registry. It deliberately remains
|
||||||
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
|
//! 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
|
//! 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 the `0.2.2` Accounts and Tokens wrappers execute real JSON-RPC requests through
|
//! 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
|
||||||
//! the shared transport path while the remaining audited methods stay staged by subsequent `0.2.x` prereleases.
|
//! requests through the shared transport path while the `0.2.3` and `0.2.4` audited families remain staged.
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
|
const MAX_GET_SLOT_LEADERS: u64 = 5_000;
|
||||||
|
|
||||||
/// Contact information returned for one cluster node.
|
/// Contact information returned for one cluster node.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -295,12 +297,10 @@ impl SolanaLeaderScheduleConfig {
|
|||||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||||
return self.commitment;
|
return self.commitment;
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
pub(crate) const fn is_empty(&self) -> bool {
|
||||||
fn is_empty(&self) -> bool {
|
|
||||||
return self.identity.is_none() && self.commitment.is_none();
|
return self.identity.is_none() && self.commitment.is_none();
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||||
fn to_json_value(&self) -> serde_json::Value {
|
|
||||||
let mut object = serde_json::Map::new();
|
let mut object = serde_json::Map::new();
|
||||||
if let std::option::Option::Some(identity) = self.identity.as_ref() {
|
if let std::option::Option::Some(identity) = self.identity.as_ref() {
|
||||||
object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string()));
|
object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string()));
|
||||||
@@ -335,7 +335,6 @@ impl Default for SolanaLeaderScheduleRequest {
|
|||||||
impl SolanaLeaderScheduleRequest {
|
impl SolanaLeaderScheduleRequest {
|
||||||
/// Serializes the typed overload to the exact positional JSON-RPC params.
|
/// Serializes the typed overload to the exact positional JSON-RPC params.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_params(&self) -> std::vec::Vec<serde_json::Value> {
|
pub(crate) fn to_json_params(&self) -> std::vec::Vec<serde_json::Value> {
|
||||||
return match self {
|
return match self {
|
||||||
Self::CurrentEpoch(std::option::Option::None) => std::vec::Vec::new(),
|
Self::CurrentEpoch(std::option::Option::None) => std::vec::Vec::new(),
|
||||||
@@ -362,7 +361,6 @@ impl SolanaLeaderSchedule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes a leader schedule map from the Solana JSON wire shape.
|
/// Decodes a leader schedule map from the Solana JSON wire shape.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<std::collections::BTreeMap<std::string::String, std::vec::Vec<usize>>>(method, value);
|
let decoded = crate::decode_wire_json::<std::collections::BTreeMap<std::string::String, std::vec::Vec<usize>>>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -422,9 +420,11 @@ impl SolanaVoteAccountsConfig {
|
|||||||
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
|
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
|
||||||
return self.delinquent_slot_distance;
|
return self.delinquent_slot_distance;
|
||||||
}
|
}
|
||||||
|
pub(crate) const fn is_empty(&self) -> bool {
|
||||||
|
return self.commitment.is_none() && self.vote_pubkey.is_none() && self.keep_unstaked_delinquents.is_none() && self.delinquent_slot_distance.is_none();
|
||||||
|
}
|
||||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||||
let mut object = serde_json::Map::new();
|
let mut object = serde_json::Map::new();
|
||||||
if let std::option::Option::Some(commitment) = self.commitment {
|
if let std::option::Option::Some(commitment) = self.commitment {
|
||||||
@@ -531,7 +531,6 @@ impl SolanaVoteAccountInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes one vote-account record from the Solana JSON wire shape.
|
/// Decodes one vote-account 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> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<WireVoteAccountInfo>(method, value);
|
let decoded = crate::decode_wire_json::<WireVoteAccountInfo>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -586,7 +585,6 @@ impl SolanaVoteAccountStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes the complete vote-account status response from the Solana JSON wire shape.
|
/// Decodes the complete vote-account status response from the Solana JSON wire shape.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<WireVoteAccountStatus>(method, value);
|
let decoded = crate::decode_wire_json::<WireVoteAccountStatus>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -621,7 +619,7 @@ struct WireIdentity {
|
|||||||
impl crate::HttpTransportPool {
|
impl crate::HttpTransportPool {
|
||||||
/// Executes typed `getClusterNodes` through the common KSP HTTP transport path.
|
/// Executes typed `getClusterNodes` through the common KSP HTTP transport path.
|
||||||
pub async fn get_cluster_nodes(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaClusterNode>> {
|
pub async fn get_cluster_nodes(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaClusterNode>> {
|
||||||
let value = self.execute_cluster_simple_rpc("getClusterNodes", role, std::vec::Vec::new()).await;
|
let value = self.execute_cluster_rpc("getClusterNodes", role, std::vec::Vec::new()).await;
|
||||||
let value = match value {
|
let value = match value {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
@@ -649,12 +647,8 @@ impl crate::HttpTransportPool {
|
|||||||
config: std::option::Option<&crate::SolanaContextConfig>,
|
config: std::option::Option<&crate::SolanaContextConfig>,
|
||||||
) -> ksp_core_lib::Result<crate::SolanaEpochInfo> {
|
) -> ksp_core_lib::Result<crate::SolanaEpochInfo> {
|
||||||
let mut params = std::vec::Vec::new();
|
let mut params = std::vec::Vec::new();
|
||||||
if let std::option::Option::Some(config) = config
|
push_context_config(&mut params, config);
|
||||||
&& (config.commitment().is_some() || config.min_context_slot().is_some())
|
let value = self.execute_cluster_rpc("getEpochInfo", role, params).await;
|
||||||
{
|
|
||||||
params.push(config.to_json_value());
|
|
||||||
}
|
|
||||||
let value = self.execute_cluster_simple_rpc("getEpochInfo", role, params).await;
|
|
||||||
return match value {
|
return match value {
|
||||||
std::result::Result::Ok(value) => crate::SolanaEpochInfo::decode_wire("getEpochInfo", value),
|
std::result::Result::Ok(value) => crate::SolanaEpochInfo::decode_wire("getEpochInfo", value),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
@@ -663,7 +657,7 @@ impl crate::HttpTransportPool {
|
|||||||
|
|
||||||
/// Executes typed `getEpochSchedule` through the common KSP HTTP transport path.
|
/// Executes typed `getEpochSchedule` through the common KSP HTTP transport path.
|
||||||
pub async fn get_epoch_schedule(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaEpochSchedule> {
|
pub async fn get_epoch_schedule(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaEpochSchedule> {
|
||||||
let value = self.execute_cluster_simple_rpc("getEpochSchedule", role, std::vec::Vec::new()).await;
|
let value = self.execute_cluster_rpc("getEpochSchedule", role, std::vec::Vec::new()).await;
|
||||||
return match value {
|
return match value {
|
||||||
std::result::Result::Ok(value) => crate::SolanaEpochSchedule::decode_wire("getEpochSchedule", value),
|
std::result::Result::Ok(value) => crate::SolanaEpochSchedule::decode_wire("getEpochSchedule", value),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
@@ -672,7 +666,7 @@ impl crate::HttpTransportPool {
|
|||||||
|
|
||||||
/// Executes typed `getHighestSnapshotSlot` through the common KSP HTTP transport path.
|
/// Executes typed `getHighestSnapshotSlot` through the common KSP HTTP transport path.
|
||||||
pub async fn get_highest_snapshot_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaSnapshotSlotInfo> {
|
pub async fn get_highest_snapshot_slot(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<crate::SolanaSnapshotSlotInfo> {
|
||||||
let value = self.execute_cluster_simple_rpc("getHighestSnapshotSlot", role, std::vec::Vec::new()).await;
|
let value = self.execute_cluster_rpc("getHighestSnapshotSlot", role, std::vec::Vec::new()).await;
|
||||||
return match value {
|
return match value {
|
||||||
std::result::Result::Ok(value) => crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", value),
|
std::result::Result::Ok(value) => crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", value),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
@@ -681,7 +675,7 @@ impl crate::HttpTransportPool {
|
|||||||
|
|
||||||
/// Executes typed `getIdentity` through the common KSP HTTP transport path.
|
/// Executes typed `getIdentity` through the common KSP HTTP transport path.
|
||||||
pub async fn get_identity(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
pub async fn get_identity(&self, role: &crate::HttpRoleName) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||||
let value = self.execute_cluster_simple_rpc("getIdentity", role, std::vec::Vec::new()).await;
|
let value = self.execute_cluster_rpc("getIdentity", role, std::vec::Vec::new()).await;
|
||||||
let value = match value {
|
let value = match value {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
@@ -704,15 +698,101 @@ impl crate::HttpTransportPool {
|
|||||||
return self.get_cluster_simple_slot("getMaxShredInsertSlot", role).await;
|
return self.get_cluster_simple_slot("getMaxShredInsertSlot", role).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getLeaderSchedule` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_leader_schedule(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
request: &crate::SolanaLeaderScheduleRequest,
|
||||||
|
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaLeaderSchedule>> {
|
||||||
|
let value = self.execute_cluster_rpc("getLeaderSchedule", role, request.to_json_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 schedule = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", value);
|
||||||
|
return match schedule {
|
||||||
|
std::result::Result::Ok(schedule) => std::result::Result::Ok(std::option::Option::Some(schedule)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getSlot` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_slot(&self, role: &crate::HttpRoleName, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<u64> {
|
||||||
|
let mut params = std::vec::Vec::new();
|
||||||
|
push_context_config(&mut params, config);
|
||||||
|
let value = self.execute_cluster_rpc("getSlot", role, params).await;
|
||||||
|
return match value {
|
||||||
|
std::result::Result::Ok(value) => crate::decode_wire_json::<u64>("getSlot", value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getSlotLeader` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_slot_leader(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
config: std::option::Option<&crate::SolanaContextConfig>,
|
||||||
|
) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||||
|
let mut params = std::vec::Vec::new();
|
||||||
|
push_context_config(&mut params, config);
|
||||||
|
let value = self.execute_cluster_rpc("getSlotLeader", 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 decoded = crate::decode_wire_json::<std::string::String>("getSlotLeader", value);
|
||||||
|
let leader = match decoded {
|
||||||
|
std::result::Result::Ok(leader) => leader,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return crate::parse_wire_pubkey("getSlotLeader", "leader", leader.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getSlotLeaders` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_slot_leaders(&self, role: &crate::HttpRoleName, start_slot: u64, limit: u64) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
|
||||||
|
if limit == 0 || limit > MAX_GET_SLOT_LEADERS {
|
||||||
|
return invalid_cluster_parameters("getSlotLeaders", "getSlotLeaders limit must be between 1 and 5000", "limit", limit);
|
||||||
|
}
|
||||||
|
let params = std::vec![serde_json::json!(start_slot), serde_json::json!(limit)];
|
||||||
|
let value = self.execute_cluster_rpc("getSlotLeaders", 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_pubkey_list("getSlotLeaders", "leader", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getVoteAccounts` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_vote_accounts(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
config: std::option::Option<&crate::SolanaVoteAccountsConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::SolanaVoteAccountStatus> {
|
||||||
|
let mut params = std::vec::Vec::new();
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& !config.is_empty()
|
||||||
|
{
|
||||||
|
params.push(config.to_json_value());
|
||||||
|
}
|
||||||
|
let value = self.execute_cluster_rpc("getVoteAccounts", role, params).await;
|
||||||
|
return match value {
|
||||||
|
std::result::Result::Ok(value) => crate::SolanaVoteAccountStatus::decode_wire("getVoteAccounts", value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_cluster_simple_slot(&self, method_name: &'static str, role: &crate::HttpRoleName) -> ksp_core_lib::Result<u64> {
|
async fn get_cluster_simple_slot(&self, method_name: &'static str, role: &crate::HttpRoleName) -> ksp_core_lib::Result<u64> {
|
||||||
let value = self.execute_cluster_simple_rpc(method_name, role, std::vec::Vec::new()).await;
|
let value = self.execute_cluster_rpc(method_name, role, std::vec::Vec::new()).await;
|
||||||
return match value {
|
return match value {
|
||||||
std::result::Result::Ok(value) => crate::decode_wire_json::<u64>(method_name, value),
|
std::result::Result::Ok(value) => crate::decode_wire_json::<u64>(method_name, value),
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_cluster_simple_rpc(
|
async fn execute_cluster_rpc(
|
||||||
&self,
|
&self,
|
||||||
method_name: &'static str,
|
method_name: &'static str,
|
||||||
role: &crate::HttpRoleName,
|
role: &crate::HttpRoleName,
|
||||||
@@ -727,6 +807,40 @@ impl crate::HttpTransportPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_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_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
|
||||||
|
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(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 pubkeys = std::vec::Vec::with_capacity(values.len());
|
||||||
|
for value in values {
|
||||||
|
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
|
||||||
|
match pubkey {
|
||||||
|
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(pubkeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> 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 cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||||
let descriptor = crate::find_http_rpc_method(method);
|
let descriptor = crate::find_http_rpc_method(method);
|
||||||
return match descriptor {
|
return match descriptor {
|
||||||
@@ -803,7 +917,6 @@ struct WireSnapshotSlotInfo {
|
|||||||
incremental: std::option::Option<u64>,
|
incremental: std::option::Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct WireVoteAccountInfo {
|
struct WireVoteAccountInfo {
|
||||||
@@ -819,7 +932,6 @@ struct WireVoteAccountInfo {
|
|||||||
root_slot: u64,
|
root_slot: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct WireVoteAccountStatus {
|
struct WireVoteAccountStatus {
|
||||||
current: std::vec::Vec<serde_json::Value>,
|
current: std::vec::Vec<serde_json::Value>,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -232,3 +232,14 @@ fn public_pre_005_simple_cluster_wrappers_are_available_from_crate_root() {
|
|||||||
let _get_max_retransmit_slot = ksp_onchain_transport_lib::HttpTransportPool::get_max_retransmit_slot;
|
let _get_max_retransmit_slot = ksp_onchain_transport_lib::HttpTransportPool::get_max_retransmit_slot;
|
||||||
let _get_max_shred_insert_slot = ksp_onchain_transport_lib::HttpTransportPool::get_max_shred_insert_slot;
|
let _get_max_shred_insert_slot = ksp_onchain_transport_lib::HttpTransportPool::get_max_shred_insert_slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_pre_006_remaining_cluster_wrappers_are_available_from_crate_root() {
|
||||||
|
let _get_leader_schedule = ksp_onchain_transport_lib::HttpTransportPool::get_leader_schedule;
|
||||||
|
let _get_slot = ksp_onchain_transport_lib::HttpTransportPool::get_slot;
|
||||||
|
let _get_slot_leader = ksp_onchain_transport_lib::HttpTransportPool::get_slot_leader;
|
||||||
|
let _get_slot_leaders = ksp_onchain_transport_lib::HttpTransportPool::get_slot_leaders;
|
||||||
|
let _get_vote_accounts = ksp_onchain_transport_lib::HttpTransportPool::get_vote_accounts;
|
||||||
|
let request = ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::default();
|
||||||
|
assert!(matches!(request, ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::None)));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||||
|
|
||||||
@@ -111,3 +111,35 @@ fn release_pre_005_simple_cluster_subset_is_exact_and_retry_safe() {
|
|||||||
assert_eq!(expected.len(), 7);
|
assert_eq!(expected.len(), 7);
|
||||||
assert_eq!(deferred.len(), 5);
|
assert_eq!(deferred.len(), 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_pre_006_completes_cluster_subset_exactly_and_retry_safe() {
|
||||||
|
let mut expected = std::vec![
|
||||||
|
"getClusterNodes",
|
||||||
|
"getEpochInfo",
|
||||||
|
"getEpochSchedule",
|
||||||
|
"getHighestSnapshotSlot",
|
||||||
|
"getIdentity",
|
||||||
|
"getLeaderSchedule",
|
||||||
|
"getMaxRetransmitSlot",
|
||||||
|
"getMaxShredInsertSlot",
|
||||||
|
"getSlot",
|
||||||
|
"getSlotLeader",
|
||||||
|
"getSlotLeaders",
|
||||||
|
"getVoteAccounts",
|
||||||
|
];
|
||||||
|
let mut actual = std::vec::Vec::new();
|
||||||
|
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
|
||||||
|
if descriptor.coverage_release() == ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_2
|
||||||
|
&& descriptor.category() == ksp_onchain_transport_lib::HttpRpcCategory::Cluster
|
||||||
|
{
|
||||||
|
actual.push(descriptor.method());
|
||||||
|
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||||
|
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actual.sort_unstable();
|
||||||
|
expected.sort_unstable();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
assert_eq!(actual.len(), 12);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() {
|
fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() {
|
||||||
@@ -52,7 +52,7 @@ fn leader_schedule_request_encodes_current_epoch_and_slot_overloads_without_ambi
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn staged_epoch_snapshot_and_leader_helpers_preserve_wire_shapes() {
|
fn epoch_snapshot_and_leader_helpers_preserve_wire_shapes() {
|
||||||
let epoch = crate::SolanaEpochInfo::decode_wire(
|
let epoch = crate::SolanaEpochInfo::decode_wire(
|
||||||
"getEpochInfo",
|
"getEpochInfo",
|
||||||
serde_json::json!({"absoluteSlot":10,"blockHeight":9,"epoch":2,"slotIndex":3,"slotsInEpoch":32,"transactionCount":null}),
|
serde_json::json!({"absoluteSlot":10,"blockHeight":9,"epoch":2,"slotIndex":3,"slotsInEpoch":32,"transactionCount":null}),
|
||||||
@@ -78,7 +78,7 @@ fn staged_epoch_snapshot_and_leader_helpers_preserve_wire_shapes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn staged_vote_status_and_config_helpers_preserve_wire_shapes() {
|
fn vote_status_and_config_helpers_preserve_wire_shapes() {
|
||||||
let vote_pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
let vote_pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
let config = crate::SolanaVoteAccountsConfig::new(
|
let config = crate::SolanaVoteAccountsConfig::new(
|
||||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||||
@@ -321,3 +321,168 @@ async fn typed_get_max_cluster_slots_decode_u64_without_params() {
|
|||||||
assert_eq!(shred_body["method"], serde_json::json!("getMaxShredInsertSlot"));
|
assert_eq!(shred_body["method"], serde_json::json!("getMaxShredInsertSlot"));
|
||||||
assert_eq!(shred_body["params"], serde_json::json!([]));
|
assert_eq!(shred_body["params"], serde_json::json!([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_leader_schedule_serializes_slot_config_and_decodes_map() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_leader_schedule.success.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let config =
|
||||||
|
crate::SolanaLeaderScheduleConfig::new(std::option::Option::Some(identity.clone()), std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||||
|
let request = crate::SolanaLeaderScheduleRequest::Slot { slot: 430_000_000, config: std::option::Option::Some(config) };
|
||||||
|
let schedule = pool
|
||||||
|
.get_leader_schedule(&crate::HttpRoleName::new("default"), &request)
|
||||||
|
.await
|
||||||
|
.expect("leader schedule fixture must succeed")
|
||||||
|
.expect("leader schedule fixture must not be null");
|
||||||
|
assert_eq!(schedule.entries().get(&identity), std::option::Option::Some(&std::vec![0, 2, 4]));
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("getLeaderSchedule"));
|
||||||
|
assert_eq!(body["params"], serde_json::json!([430000000,{"identity":"11111111111111111111111111111111","commitment":"finalized"}]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_leader_schedule_preserves_null_result() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_leader_schedule.null.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let schedule = pool
|
||||||
|
.get_leader_schedule(&crate::HttpRoleName::new("default"), &crate::SolanaLeaderScheduleRequest::default())
|
||||||
|
.await
|
||||||
|
.expect("nullable leader schedule fixture must succeed");
|
||||||
|
assert_eq!(schedule, std::option::Option::None);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_slot_serializes_context_config_and_omits_empty_config() {
|
||||||
|
let (configured_url, configured_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json"));
|
||||||
|
let configured_pool = pool_for_url(configured_url.as_str());
|
||||||
|
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(429_999_999));
|
||||||
|
let slot = configured_pool.get_slot(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config)).await.expect("slot fixture must succeed");
|
||||||
|
assert_eq!(slot, 430_000_020);
|
||||||
|
let request = configured_handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"confirmed","minContextSlot":429999999}]));
|
||||||
|
|
||||||
|
let (empty_url, empty_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json"));
|
||||||
|
let empty_pool = pool_for_url(empty_url.as_str());
|
||||||
|
let empty = crate::SolanaContextConfig::default();
|
||||||
|
let slot = empty_pool
|
||||||
|
.get_slot(&crate::HttpRoleName::new("default"), std::option::Option::Some(&empty))
|
||||||
|
.await
|
||||||
|
.expect("slot fixture with empty config must succeed");
|
||||||
|
assert_eq!(slot, 430_000_020);
|
||||||
|
let request = empty_handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_slot_leader_serializes_context_config_and_decodes_pubkey() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leader.success.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000));
|
||||||
|
let leader = pool
|
||||||
|
.get_slot_leader(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("slot leader fixture must succeed");
|
||||||
|
assert_eq!(leader.to_string(), "ComputeBudget111111111111111111111111111111");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"finalized","minContextSlot":430000000}]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_slot_leader_rejects_invalid_wire_pubkey() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leader.invalid_pubkey.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let result = pool.get_slot_leader(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||||
|
let error = result.expect_err("invalid slot leader must reject typed response");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_slot_leaders_preserves_order_and_serializes_range() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leaders.success.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let leaders = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 430_000_020, 2).await.expect("slot leaders fixture must succeed");
|
||||||
|
assert_eq!(leaders.len(), 2);
|
||||||
|
assert_eq!(leaders[0].to_string(), "11111111111111111111111111111111");
|
||||||
|
assert_eq!(leaders[1].to_string(), "ComputeBudget111111111111111111111111111111");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("getSlotLeaders"));
|
||||||
|
assert_eq!(body["params"], serde_json::json!([430000020, 2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_slot_leaders_rejects_out_of_range_limit_before_io() {
|
||||||
|
let pool = pool_for_url("http://127.0.0.1:9");
|
||||||
|
let zero = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 0).await;
|
||||||
|
let zero_error = zero.expect_err("zero slot-leader limit must reject before I/O");
|
||||||
|
assert_eq!(zero_error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
let above = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 5_001).await;
|
||||||
|
let above_error = above.expect_err("slot-leader limit above 5000 must reject before I/O");
|
||||||
|
assert_eq!(above_error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_slot_leaders_rejects_invalid_wire_pubkey() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_slot_leaders.invalid_pubkey.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let result = pool.get_slot_leaders(&crate::HttpRoleName::new("default"), 1, 2).await;
|
||||||
|
let error = result.expect_err("invalid slot leader list item must reject typed response");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_vote_accounts_serializes_full_config_and_preserves_optional_bps() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.success.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let vote_pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let config = crate::SolanaVoteAccountsConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||||
|
std::option::Option::Some(vote_pubkey),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(128),
|
||||||
|
);
|
||||||
|
let status = pool
|
||||||
|
.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("vote accounts fixture must succeed");
|
||||||
|
assert_eq!(status.current().len(), 1);
|
||||||
|
assert_eq!(status.delinquent().len(), 1);
|
||||||
|
assert_eq!(status.current()[0].inflation_rewards_commission_bps(), std::option::Option::Some(750));
|
||||||
|
assert_eq!(status.current()[0].epoch_credits().len(), 2);
|
||||||
|
assert_eq!(status.delinquent()[0].inflation_rewards_commission_bps(), std::option::Option::None);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(
|
||||||
|
request_body(request.as_str())["params"],
|
||||||
|
serde_json::json!([{"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_vote_accounts_omits_explicitly_empty_config() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.success.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let config = crate::SolanaVoteAccountsConfig::default();
|
||||||
|
let status = pool
|
||||||
|
.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("vote accounts fixture with empty config must succeed");
|
||||||
|
assert_eq!(status.current().len(), 1);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_vote_accounts_rejects_invalid_wire_pubkey() {
|
||||||
|
let (url, handle) = serve_once(include_str!("../fixtures/http/get_vote_accounts.invalid_pubkey.json"));
|
||||||
|
let pool = pool_for_url(url.as_str());
|
||||||
|
let result = pool.get_vote_accounts(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
|
||||||
|
let error = result.expect_err("invalid vote-account pubkey must reject typed response");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|||||||
298
deltas/0.2.2/pre.006.md
Normal file
298
deltas/0.2.2/pre.006.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<!-- file: deltas/0.2.2/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.2-pre.006` — cinq wrappers HTTP Cluster restants typés
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée localement par l'opérateur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.2-pre.005
|
||||||
|
workspace.package.version = "0.2.2-pre.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation opérateur du 2026-08-18 a confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats Transport de cette base :
|
||||||
|
|
||||||
|
```text
|
||||||
|
116 unit tests
|
||||||
|
12 public API tests
|
||||||
|
5 release completeness tests
|
||||||
|
0 warning signalé par check/clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
Le plan canonique reste `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3.
|
||||||
|
|
||||||
|
## Contrôle documentaire avant nouvelle tranche
|
||||||
|
|
||||||
|
Le découpage durable reste cohérent :
|
||||||
|
|
||||||
|
- `ROADMAP.md` conserve `0.2.2` en cours `[/]` ;
|
||||||
|
- le plan `009` attribue exactement à `pre.006` les cinq méthodes Cluster restantes ;
|
||||||
|
- les 5 Accounts de `pre.003`, les 5 Tokens de `pre.004` et les 7 Cluster simples de `pre.005` restent inchangés ;
|
||||||
|
- `pre.007` reste la tranche de complétude/smoke/docs/préparation stable prévue ;
|
||||||
|
- aucun index documentaire durable ne nécessite de modification à cette étape.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Implémenter les cinq wrappers publics typés Cluster restants :
|
||||||
|
|
||||||
|
```text
|
||||||
|
getLeaderSchedule
|
||||||
|
getSlot
|
||||||
|
getSlotLeader
|
||||||
|
getSlotLeaders
|
||||||
|
getVoteAccounts
|
||||||
|
```
|
||||||
|
|
||||||
|
Après cette tranche, les 22 méthodes HTTP Accounts + Tokens + Cluster attribuées à `0.2.2` possèdent toutes leur wrapper typé KSP.
|
||||||
|
|
||||||
|
Tous les wrappers passent par la foundation HTTP commune :
|
||||||
|
|
||||||
|
```text
|
||||||
|
wrapper typé
|
||||||
|
-> descriptor audité
|
||||||
|
-> execute_standard_rpc
|
||||||
|
-> pool/admission/retry/deadline
|
||||||
|
-> reqwest HTTP
|
||||||
|
-> JSON-RPC validation
|
||||||
|
-> décodage DTO KSP
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun wrapper Cluster ne contacte `reqwest` directement.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Conformément au cycle prerelease KSP :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.2-pre.5 -> 0.2.2-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n'est ajoutée ou retirée.
|
||||||
|
|
||||||
|
## Réaudit ciblé du contrat courant
|
||||||
|
|
||||||
|
Le contrat préparé dans le plan `009` a été recroisé avec la source primaire Agave `v4.2.1`, notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
rpc-client-types/src/config.rs
|
||||||
|
rpc/src/rpc.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Les invariants utiles à cette tranche restent :
|
||||||
|
|
||||||
|
- `getLeaderSchedule` accepte l'overload slot/config déjà modélisé par `SolanaLeaderScheduleRequest` ;
|
||||||
|
- `RpcLeaderScheduleConfig` contient `identity` et `commitment` ;
|
||||||
|
- le résultat de `getLeaderSchedule` reste nullable ;
|
||||||
|
- `getSlot` et `getSlotLeader` utilisent la config commune `commitment/minContextSlot` ;
|
||||||
|
- `getSlotLeaders` prend `startSlot + limit`, avec la limite `1..=5000` retenue par le plan ;
|
||||||
|
- `RpcGetVoteAccountsConfig` contient `votePubkey`, `commitment`, `keepUnstakedDelinquents`, `delinquentSlotDistance` ;
|
||||||
|
- `getVoteAccounts` renvoie les groupes `current` et `delinquent` ;
|
||||||
|
- `inflationRewardsCommissionBps` reste optionnel pour compatibilité avec les noeuds antérieurs ;
|
||||||
|
- l'historique `epochCredits` reste exposé comme historique RPC borné, sans être interprété comme historique complet du vote account.
|
||||||
|
|
||||||
|
## Surface typée Cluster finale
|
||||||
|
|
||||||
|
### `getLeaderSchedule`
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + SolanaLeaderScheduleRequest -> Option<SolanaLeaderSchedule>
|
||||||
|
```
|
||||||
|
|
||||||
|
`SolanaLeaderScheduleRequest` conserve les deux formes cohérentes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
CurrentEpoch(config?)
|
||||||
|
Slot { slot, config? }
|
||||||
|
```
|
||||||
|
|
||||||
|
Un config vide est omis. Un résultat JSON `null` devient `None`. Une map non nulle valide chaque identité et la convertit en `ksp_core_lib::Pubkey`.
|
||||||
|
|
||||||
|
### `getSlot`
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + Option<SolanaContextConfig> -> u64
|
||||||
|
```
|
||||||
|
|
||||||
|
La config commune est sérialisée uniquement lorsqu'elle contient `commitment` et/ou `minContextSlot`. Une erreur distante `MinContextSlotNotReached` reste une erreur RPC applicative.
|
||||||
|
|
||||||
|
### `getSlotLeader`
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + Option<SolanaContextConfig> -> Pubkey
|
||||||
|
```
|
||||||
|
|
||||||
|
La valeur texte du leader est validée avant exposition comme `Pubkey`, sans recopier une valeur wire invalide dans le diagnostic.
|
||||||
|
|
||||||
|
### `getSlotLeaders`
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + startSlot u64 + limit u64 -> Vec<Pubkey>
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP rejette localement avant I/O :
|
||||||
|
|
||||||
|
```text
|
||||||
|
limit == 0
|
||||||
|
limit > 5000
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ordre du tableau retourné est conservé, car chaque position correspond aux slots successifs demandés.
|
||||||
|
|
||||||
|
### `getVoteAccounts`
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + Option<SolanaVoteAccountsConfig> -> SolanaVoteAccountStatus
|
||||||
|
```
|
||||||
|
|
||||||
|
Un config vide est omis. Les deux groupes `current` et `delinquent` sont conservés, ainsi que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
votePubkey
|
||||||
|
nodePubkey
|
||||||
|
activatedStake
|
||||||
|
commission
|
||||||
|
inflationRewardsCommissionBps Option<u16>
|
||||||
|
epochVoteAccount
|
||||||
|
epochCredits Vec<SolanaEpochCredits>
|
||||||
|
lastVote
|
||||||
|
rootSlot
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune conversion métier staking/rewards n'est introduite dans Transport.
|
||||||
|
|
||||||
|
## Activation finale des helpers préparés en `pre.002`
|
||||||
|
|
||||||
|
Les helpers spécifiques aux cinq méthodes de cette tranche deviennent production-live maintenant qu'ils ont des consommateurs runtime :
|
||||||
|
|
||||||
|
- `SolanaLeaderScheduleConfig::{is_empty,to_json_value}` ;
|
||||||
|
- `SolanaLeaderScheduleRequest::to_json_params` ;
|
||||||
|
- `SolanaLeaderSchedule::decode_wire` ;
|
||||||
|
- `SolanaVoteAccountsConfig::{is_empty,to_json_value}` ;
|
||||||
|
- `SolanaVoteAccountInfo::decode_wire` ;
|
||||||
|
- `SolanaVoteAccountStatus::decode_wire` ;
|
||||||
|
- `WireVoteAccountInfo` et `WireVoteAccountStatus`.
|
||||||
|
|
||||||
|
Aucun `#[allow(dead_code)]` n'est ajouté.
|
||||||
|
|
||||||
|
Le helper privé d'exécution Cluster est renommé de `execute_cluster_simple_rpc` vers `execute_cluster_rpc`, car il dessert désormais les douze wrappers Cluster.
|
||||||
|
|
||||||
|
## Fixtures HTTP déterministes ajoutées
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.null.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.invalid_pubkey.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.invalid_pubkey.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.invalid_pubkey.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests utilisent uniquement un serveur HTTP loopback local.
|
||||||
|
|
||||||
|
## Couverture de tests ajoutée
|
||||||
|
|
||||||
|
Les tests couvrent notamment :
|
||||||
|
|
||||||
|
- sérialisation slot + config de `getLeaderSchedule` ;
|
||||||
|
- résultat nullable de `getLeaderSchedule` ;
|
||||||
|
- sérialisation `commitment/minContextSlot` de `getSlot` et `getSlotLeader` ;
|
||||||
|
- omission d'un `SolanaContextConfig` vide ;
|
||||||
|
- validation de la `Pubkey` de `getSlotLeader` ;
|
||||||
|
- conservation de l'ordre de `getSlotLeaders` ;
|
||||||
|
- rejet local des limites `0` et `5001` avant I/O ;
|
||||||
|
- rejet d'une identité invalide dans la liste `getSlotLeaders` ;
|
||||||
|
- sérialisation complète de `SolanaVoteAccountsConfig` ;
|
||||||
|
- omission d'un config vote accounts vide ;
|
||||||
|
- conservation de `inflationRewardsCommissionBps` présent et absent ;
|
||||||
|
- conservation des triples `epochCredits` ;
|
||||||
|
- rejet d'une `votePubkey` invalide.
|
||||||
|
|
||||||
|
Le test public compile explicitement les cinq nouvelles méthodes depuis `HttpTransportPool`.
|
||||||
|
|
||||||
|
Un canari release fige désormais les douze méthodes Cluster exactes de `0.2.2` et vérifie `Read + RetrySafe` sur tout le sous-ensemble.
|
||||||
|
|
||||||
|
Après application, la cible Transport attendue devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
127 unit tests
|
||||||
|
13 public API tests
|
||||||
|
6 release completeness tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rustdoc de crate
|
||||||
|
|
||||||
|
La documentation de tête de `ksp-onchain-transport-lib` est ajustée pour ne plus présenter Cluster comme une famille encore staged : après `pre.006`, les 22 wrappers `0.2.2` sont implémentés. Les familles `0.2.3` et `0.2.4` restent staged.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_leader_schedule.null.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leader.invalid_pubkey.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_slot_leaders.invalid_pubkey.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_vote_accounts.invalid_pubkey.json
|
||||||
|
deltas/0.2.2/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Documentation durable
|
||||||
|
|
||||||
|
Aucune modification de ROADMAP/plan/index n'est requise dans cette tranche : le plan existant décrit déjà ce découpage et `pre.007` reste la tranche de clôture prerelease/documentation.
|
||||||
|
|
||||||
|
`CHANGELOG.md` reste réservé à la clôture stable.
|
||||||
|
|
||||||
|
## Validation à exécuter sur le checkout opérateur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
La livraison n'affirme pas que ces commandes ont été exécutées dans l'environnement de génération.
|
||||||
|
|
||||||
|
## Critères de validation de `pre.006`
|
||||||
|
|
||||||
|
`pre.006` est validable lorsque :
|
||||||
|
|
||||||
|
- les quatre commandes ci-dessus passent sans warning nouveau ;
|
||||||
|
- les 127 unit tests passent ;
|
||||||
|
- les 13 tests public API passent ;
|
||||||
|
- les 6 tests release completeness passent ;
|
||||||
|
- aucun helper Cluster production-live n'est `dead_code` ;
|
||||||
|
- les douze wrappers Cluster `0.2.2` sont accessibles depuis `HttpTransportPool` ;
|
||||||
|
- le total `0.2.2` reste exactement 5 Accounts + 5 Tokens + 12 Cluster = 22 wrappers ;
|
||||||
|
- aucune méthode `0.2.3`/`0.2.4` n'est activée prématurément.
|
||||||
Reference in New Issue
Block a user