v0.2.2-pre.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 110
|
||||
# version: 111
|
||||
|
||||
[workspace]
|
||||
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"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.2-pre.1"
|
||||
version = "0.2.2-pre.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"lamports":1,"data":"3MN5","owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":null},
|
||||
{"lamports":2,"data":["KLUv/Q==","base64+zstd"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":1,"space":4},
|
||||
{"lamports":3,"data":{"program":"spl-token","parsed":{"type":"account","info":{"state":"initialized"}},"space":165},"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":2,"space":165}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
{"pubkey":"11111111111111111111111111111111","featureSet":123,"gossip":"127.0.0.1:8001","pubsub":null,"rpc":"127.0.0.1:8899","serveRepair":"127.0.0.1:8003","shredVersion":456,"tpu":"127.0.0.1:8004","tpuForwards":null,"tpuForwardsQuic":"127.0.0.1:8006","tpuQuic":"127.0.0.1:8005","tpuVote":null,"tvu":"127.0.0.1:8002","version":"4.2.1","clientId":"Agave"}
|
||||
@@ -0,0 +1 @@
|
||||
{"amount":"18446744073709551615","decimals":9,"uiAmount":null,"uiAmountString":"18446744073.709551615"}
|
||||
@@ -0,0 +1 @@
|
||||
{"votePubkey":"11111111111111111111111111111111","nodePubkey":"11111111111111111111111111111111","activatedStake":424242,"commission":8,"inflationRewardsCommissionBps":750,"epochVoteAccount":true,"epochCredits":[[700,100,90],[701,115,100]],"lastVote":999,"rootSlot":990}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -19,8 +19,12 @@ mod executor;
|
||||
mod json_rpc;
|
||||
mod pool;
|
||||
mod resilience;
|
||||
mod rpc_accounts;
|
||||
mod rpc_canary;
|
||||
mod rpc_cluster;
|
||||
mod rpc_common;
|
||||
mod rpc_method;
|
||||
mod rpc_tokens;
|
||||
mod settings;
|
||||
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
@@ -91,16 +95,84 @@ pub use self::resilience::evaluate_transport_retry;
|
||||
pub use self::rpc_canary::GetBalanceConfig;
|
||||
/// Typed lamport balance returned by the `getBalance` canary.
|
||||
pub use self::rpc_canary::GetBalanceResult;
|
||||
/// Commitment level accepted by the initial typed Solana HTTP canary adapters.
|
||||
pub use self::rpc_canary::SolanaCommitment;
|
||||
/// Typed genesis hash returned by the `getGenesisHash` canary.
|
||||
pub use self::rpc_canary::SolanaGenesisHash;
|
||||
/// Typed healthy result returned by the `getHealth` canary.
|
||||
pub use self::rpc_canary::SolanaNodeHealth;
|
||||
/// Typed software-version response returned by the `getVersion` canary.
|
||||
pub use self::rpc_canary::SolanaNodeVersion;
|
||||
/// Typed Solana RPC context used by the initial account canary.
|
||||
pub use self::rpc_canary::SolanaRpcContext;
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
pub use self::rpc_accounts::SolanaAccountEncoding;
|
||||
/// Typed transport-level Solana account without Program/SPL decoding.
|
||||
pub use self::rpc_accounts::SolanaAccount;
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaAccountBalance;
|
||||
/// Wire-preserving account data returned by Solana HTTP account methods.
|
||||
pub use self::rpc_accounts::SolanaAccountData;
|
||||
/// Shared account configuration used by account-info and token-account list methods.
|
||||
pub use self::rpc_accounts::SolanaAccountInfoConfig;
|
||||
/// Byte range requested from account data without decoding it locally.
|
||||
pub use self::rpc_accounts::SolanaDataSliceConfig;
|
||||
/// One public key plus its account returned by account-list RPC methods.
|
||||
pub use self::rpc_accounts::SolanaKeyedAccount;
|
||||
/// Filter accepted by `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaLargestAccountsFilter;
|
||||
/// Optional configuration for `getLargestAccounts`.
|
||||
pub use self::rpc_accounts::SolanaLargestAccountsConfig;
|
||||
/// Bytes used by a `memcmp` program-account filter.
|
||||
pub use self::rpc_accounts::SolanaMemcmpBytes;
|
||||
/// One `memcmp` filter applied to account data.
|
||||
pub use self::rpc_accounts::SolanaMemcmpFilter;
|
||||
/// Parsed account payload returned by the RPC node for `jsonParsed` account data.
|
||||
pub use self::rpc_accounts::SolanaParsedAccountData;
|
||||
/// Filter accepted by the current `getProgramAccounts` implementation.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountFilter;
|
||||
/// Configuration for `getProgramAccounts`.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountsConfig;
|
||||
/// Result union returned by `getProgramAccounts` with or without an RPC context.
|
||||
pub use self::rpc_accounts::SolanaProgramAccountsResult;
|
||||
/// Contact information returned for one cluster node.
|
||||
pub use self::rpc_cluster::SolanaClusterNode;
|
||||
/// Epoch-credit history entry returned by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaEpochCredits;
|
||||
/// Epoch information returned by `getEpochInfo`.
|
||||
pub use self::rpc_cluster::SolanaEpochInfo;
|
||||
/// Epoch schedule returned by `getEpochSchedule`.
|
||||
pub use self::rpc_cluster::SolanaEpochSchedule;
|
||||
/// Leader schedule mapping validator identities to relative epoch slot indices.
|
||||
pub use self::rpc_cluster::SolanaLeaderSchedule;
|
||||
/// Optional configuration accepted by `getLeaderSchedule`.
|
||||
pub use self::rpc_cluster::SolanaLeaderScheduleConfig;
|
||||
/// Typed parameter overload for `getLeaderSchedule`.
|
||||
pub use self::rpc_cluster::SolanaLeaderScheduleRequest;
|
||||
/// Highest full and optional incremental snapshot slots returned by `getHighestSnapshotSlot`.
|
||||
pub use self::rpc_cluster::SolanaSnapshotSlotInfo;
|
||||
/// One validator vote-account record returned by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountInfo;
|
||||
/// Current and delinquent validator vote-account groups returned by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountStatus;
|
||||
/// Configuration accepted by `getVoteAccounts`.
|
||||
pub use self::rpc_cluster::SolanaVoteAccountsConfig;
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
pub use self::rpc_common::SolanaCommitment;
|
||||
/// Optional commitment-only configuration shared by typed Solana HTTP RPC methods.
|
||||
pub use self::rpc_common::SolanaCommitmentConfig;
|
||||
/// Optional commitment and minimum-context configuration shared by typed Solana HTTP RPC methods.
|
||||
pub use self::rpc_common::SolanaContextConfig;
|
||||
/// Typed Solana RPC context shared by contextual HTTP responses.
|
||||
pub use self::rpc_common::SolanaRpcContext;
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
pub use self::rpc_common::SolanaRpcResponse;
|
||||
/// Exclusive selector accepted by token-account list RPC methods.
|
||||
pub use self::rpc_tokens::SolanaTokenAccountSelector;
|
||||
/// Token-account balance entry returned by `getTokenLargestAccounts`.
|
||||
pub use self::rpc_tokens::SolanaTokenAccountBalance;
|
||||
/// Token amount returned by Solana HTTP token RPC methods.
|
||||
pub use self::rpc_tokens::SolanaTokenAmount;
|
||||
/// Decodes one private serde wire type into the shared Transport error domain.
|
||||
pub(crate) use self::rpc_common::decode_wire_json;
|
||||
/// Parses a base58 public key without echoing its wire value into diagnostics.
|
||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
|
||||
pub use self::rpc_method::HttpRpcCategory;
|
||||
/// Release that owns typed KSP coverage for one audited HTTP RPC method.
|
||||
|
||||
632
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
Normal file
632
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
Normal file
@@ -0,0 +1,632 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
// version: 1
|
||||
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaAccountEncoding {
|
||||
/// Legacy binary/base58 request encoding.
|
||||
Binary,
|
||||
/// Base58 text encoding.
|
||||
Base58,
|
||||
/// Base64 text encoding.
|
||||
Base64,
|
||||
/// Parsed JSON representation when the RPC node has a parser for the account owner.
|
||||
JsonParsed,
|
||||
/// Base64 text containing zstd-compressed bytes.
|
||||
Base64Zstd,
|
||||
}
|
||||
|
||||
impl SolanaAccountEncoding {
|
||||
/// 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::JsonParsed => "jsonParsed",
|
||||
Self::Base64Zstd => "base64+zstd",
|
||||
};
|
||||
}
|
||||
|
||||
fn from_wire(value: &str) -> std::option::Option<Self> {
|
||||
return match value {
|
||||
"binary" => std::option::Option::Some(Self::Binary),
|
||||
"base58" => std::option::Option::Some(Self::Base58),
|
||||
"base64" => std::option::Option::Some(Self::Base64),
|
||||
"jsonParsed" => std::option::Option::Some(Self::JsonParsed),
|
||||
"base64+zstd" => std::option::Option::Some(Self::Base64Zstd),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte range requested from account data without decoding it locally.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct SolanaDataSliceConfig {
|
||||
offset: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl SolanaDataSliceConfig {
|
||||
/// Creates an account-data slice configuration.
|
||||
#[must_use]
|
||||
pub const fn new(offset: usize, length: usize) -> Self {
|
||||
return Self { offset, length };
|
||||
}
|
||||
|
||||
/// Returns the byte offset.
|
||||
#[must_use]
|
||||
pub const fn offset(&self) -> usize {
|
||||
return self.offset;
|
||||
}
|
||||
|
||||
/// Returns the requested byte length.
|
||||
#[must_use]
|
||||
pub const fn length(&self) -> usize {
|
||||
return self.length;
|
||||
}
|
||||
|
||||
fn to_json_value(self) -> serde_json::Value {
|
||||
return serde_json::json!({"offset": self.offset, "length": self.length});
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared account configuration used by account-info and token-account list methods.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaAccountInfoConfig {
|
||||
encoding: std::option::Option<crate::SolanaAccountEncoding>,
|
||||
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
|
||||
context: crate::SolanaContextConfig,
|
||||
}
|
||||
|
||||
impl SolanaAccountInfoConfig {
|
||||
/// Creates an explicit account-info configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
encoding: std::option::Option<crate::SolanaAccountEncoding>,
|
||||
data_slice: std::option::Option<crate::SolanaDataSliceConfig>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
) -> Self {
|
||||
return Self { encoding, data_slice, context: crate::SolanaContextConfig::new(commitment, min_context_slot) };
|
||||
}
|
||||
|
||||
/// Returns the optional account-data encoding.
|
||||
#[must_use]
|
||||
pub const fn encoding(&self) -> std::option::Option<crate::SolanaAccountEncoding> {
|
||||
return self.encoding;
|
||||
}
|
||||
|
||||
/// Returns the optional account-data slice.
|
||||
#[must_use]
|
||||
pub const fn data_slice(&self) -> std::option::Option<crate::SolanaDataSliceConfig> {
|
||||
return self.data_slice;
|
||||
}
|
||||
|
||||
/// 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 this config serializes to an empty JSON object.
|
||||
#[must_use]
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.encoding.is_none() && self.data_slice.is_none() && self.context.is_empty();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let context_value = self.context.to_json_value();
|
||||
let mut object = match context_value {
|
||||
serde_json::Value::Object(object) => 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()));
|
||||
}
|
||||
if let std::option::Option::Some(data_slice) = self.data_slice {
|
||||
object.insert("dataSlice".to_owned(), data_slice.to_json_value());
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter accepted by `getLargestAccounts`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaLargestAccountsFilter {
|
||||
/// Return only circulating accounts.
|
||||
Circulating,
|
||||
/// Return only non-circulating accounts.
|
||||
NonCirculating,
|
||||
}
|
||||
|
||||
impl SolanaLargestAccountsFilter {
|
||||
fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Circulating => "circulating",
|
||||
Self::NonCirculating => "nonCirculating",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional configuration for `getLargestAccounts`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaLargestAccountsConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
filter: std::option::Option<crate::SolanaLargestAccountsFilter>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl SolanaLargestAccountsConfig {
|
||||
/// Creates a largest-accounts configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
filter: std::option::Option<crate::SolanaLargestAccountsFilter>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
) -> Self {
|
||||
return Self { commitment, filter, sort_results };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns the optional circulating-account filter.
|
||||
#[must_use]
|
||||
pub const fn filter(&self) -> std::option::Option<crate::SolanaLargestAccountsFilter> {
|
||||
return self.filter;
|
||||
}
|
||||
|
||||
/// Returns the optional server-side result-sorting request.
|
||||
#[must_use]
|
||||
pub const fn sort_results(&self) -> std::option::Option<bool> {
|
||||
return self.sort_results;
|
||||
}
|
||||
|
||||
/// 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(filter) = self.filter {
|
||||
object.insert("filter".to_owned(), serde_json::Value::String(filter.as_str().to_owned()));
|
||||
}
|
||||
if let std::option::Option::Some(sort_results) = self.sort_results {
|
||||
object.insert("sortResults".to_owned(), serde_json::Value::Bool(sort_results));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes used by a `memcmp` program-account filter.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaMemcmpBytes {
|
||||
/// Base58-encoded bytes.
|
||||
Base58(std::string::String),
|
||||
/// Base64-encoded bytes.
|
||||
Base64(std::string::String),
|
||||
/// Raw byte array.
|
||||
Bytes(std::vec::Vec<u8>),
|
||||
}
|
||||
|
||||
/// One `memcmp` filter applied to account data.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaMemcmpFilter {
|
||||
offset: usize,
|
||||
bytes: crate::SolanaMemcmpBytes,
|
||||
}
|
||||
|
||||
impl SolanaMemcmpFilter {
|
||||
/// Creates a `memcmp` filter without locally decoding encoded string data.
|
||||
#[must_use]
|
||||
pub fn new(offset: usize, bytes: crate::SolanaMemcmpBytes) -> Self {
|
||||
return Self { offset, bytes };
|
||||
}
|
||||
|
||||
/// Returns the account-data byte offset.
|
||||
#[must_use]
|
||||
pub const fn offset(&self) -> usize {
|
||||
return self.offset;
|
||||
}
|
||||
|
||||
/// Returns the encoded or raw bytes.
|
||||
#[must_use]
|
||||
pub const fn bytes(&self) -> &crate::SolanaMemcmpBytes {
|
||||
return &self.bytes;
|
||||
}
|
||||
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match &self.bytes {
|
||||
crate::SolanaMemcmpBytes::Base58(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base58"}),
|
||||
crate::SolanaMemcmpBytes::Base64(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "base64"}),
|
||||
crate::SolanaMemcmpBytes::Bytes(bytes) => serde_json::json!({"offset": self.offset, "bytes": bytes, "encoding": "bytes"}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter accepted by the current `getProgramAccounts` implementation.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaProgramAccountFilter {
|
||||
/// Require an exact account data size.
|
||||
DataSize(u64),
|
||||
/// Compare bytes at one account-data offset.
|
||||
Memcmp(crate::SolanaMemcmpFilter),
|
||||
/// Require a valid SPL Token account-state layout according to the RPC implementation.
|
||||
TokenAccountState,
|
||||
}
|
||||
|
||||
impl SolanaProgramAccountFilter {
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::DataSize(size) => serde_json::json!({"dataSize": size}),
|
||||
Self::Memcmp(filter) => serde_json::json!({"memcmp": filter.to_json_value()}),
|
||||
Self::TokenAccountState => serde_json::Value::String("tokenAccountState".to_owned()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for `getProgramAccounts` built from the shared account config plus program filters.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaProgramAccountsConfig {
|
||||
account_config: crate::SolanaAccountInfoConfig,
|
||||
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
|
||||
with_context: std::option::Option<bool>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
impl SolanaProgramAccountsConfig {
|
||||
/// Creates a program-accounts configuration.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
account_config: crate::SolanaAccountInfoConfig,
|
||||
filters: std::vec::Vec<crate::SolanaProgramAccountFilter>,
|
||||
with_context: std::option::Option<bool>,
|
||||
sort_results: std::option::Option<bool>,
|
||||
) -> Self {
|
||||
return Self { account_config, filters, with_context, sort_results };
|
||||
}
|
||||
|
||||
/// Returns the shared account configuration.
|
||||
#[must_use]
|
||||
pub const fn account_config(&self) -> &crate::SolanaAccountInfoConfig {
|
||||
return &self.account_config;
|
||||
}
|
||||
|
||||
/// Returns the ordered program-account filters.
|
||||
#[must_use]
|
||||
pub fn filters(&self) -> &[crate::SolanaProgramAccountFilter] {
|
||||
return self.filters.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the optional context-wrapper request.
|
||||
#[must_use]
|
||||
pub const fn with_context(&self) -> std::option::Option<bool> {
|
||||
return self.with_context;
|
||||
}
|
||||
|
||||
/// Returns the optional server-side sorting request.
|
||||
#[must_use]
|
||||
pub const fn sort_results(&self) -> std::option::Option<bool> {
|
||||
return self.sort_results;
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let account_value = self.account_config.to_json_value();
|
||||
let mut object = match account_value {
|
||||
serde_json::Value::Object(object) => object,
|
||||
_ => serde_json::Map::new(),
|
||||
};
|
||||
if !self.filters.is_empty() {
|
||||
let values = self.filters.iter().map(crate::SolanaProgramAccountFilter::to_json_value).collect::<std::vec::Vec<_>>();
|
||||
object.insert("filters".to_owned(), serde_json::Value::Array(values));
|
||||
}
|
||||
if let std::option::Option::Some(with_context) = self.with_context {
|
||||
object.insert("withContext".to_owned(), serde_json::Value::Bool(with_context));
|
||||
}
|
||||
if let std::option::Option::Some(sort_results) = self.sort_results {
|
||||
object.insert("sortResults".to_owned(), serde_json::Value::Bool(sort_results));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed account payload returned by the RPC node for `jsonParsed` account data.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaParsedAccountData {
|
||||
program: std::string::String,
|
||||
parsed: serde_json::Value,
|
||||
space: u64,
|
||||
}
|
||||
|
||||
impl SolanaParsedAccountData {
|
||||
/// Returns the parser/program label reported by the RPC node.
|
||||
#[must_use]
|
||||
pub fn program(&self) -> &str {
|
||||
return self.program.as_str();
|
||||
}
|
||||
|
||||
/// Returns the parsed JSON payload without converting it to a Program/SPL domain model.
|
||||
#[must_use]
|
||||
pub const fn parsed(&self) -> &serde_json::Value {
|
||||
return &self.parsed;
|
||||
}
|
||||
|
||||
/// Returns the account-data space reported inside the parsed payload.
|
||||
#[must_use]
|
||||
pub const fn space(&self) -> u64 {
|
||||
return self.space;
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire-preserving account data returned by Solana HTTP account methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaAccountData {
|
||||
/// Legacy single-string binary form retained for backwards compatibility.
|
||||
LegacyBinary(std::string::String),
|
||||
/// Encoded tuple `[data, encoding]`.
|
||||
Encoded {
|
||||
/// Encoded account bytes.
|
||||
data: std::string::String,
|
||||
/// Encoding label returned by the RPC node.
|
||||
encoding: crate::SolanaAccountEncoding,
|
||||
},
|
||||
/// Parsed JSON object returned by the RPC node.
|
||||
JsonParsed(crate::SolanaParsedAccountData),
|
||||
}
|
||||
|
||||
impl SolanaAccountData {
|
||||
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireAccountData>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match wire {
|
||||
WireAccountData::LegacyBinary(value) => std::result::Result::Ok(Self::LegacyBinary(value)),
|
||||
WireAccountData::JsonParsed(value) => std::result::Result::Ok(Self::JsonParsed(crate::SolanaParsedAccountData {
|
||||
program: value.program,
|
||||
parsed: value.parsed,
|
||||
space: value.space,
|
||||
})),
|
||||
WireAccountData::Encoded((data, encoding)) => {
|
||||
let parsed = crate::SolanaAccountEncoding::from_wire(encoding.as_str());
|
||||
let encoding = match parsed {
|
||||
std::option::Option::Some(encoding) => encoding,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an unknown encoding")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
}
|
||||
};
|
||||
if encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::JsonParsed {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "account data tuple uses an invalid tuple encoding")
|
||||
.with_context("rpc_method", method),
|
||||
);
|
||||
}
|
||||
std::result::Result::Ok(Self::Encoded { data, encoding })
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed transport-level Solana account without Program/SPL decoding.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaAccount {
|
||||
lamports: u64,
|
||||
data: crate::SolanaAccountData,
|
||||
owner: ksp_core_lib::Pubkey,
|
||||
executable: bool,
|
||||
rent_epoch: u64,
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaAccount {
|
||||
/// Returns the account balance in lamports.
|
||||
#[must_use]
|
||||
pub const fn lamports(&self) -> u64 {
|
||||
return self.lamports;
|
||||
}
|
||||
|
||||
/// Returns the wire-preserving account data.
|
||||
#[must_use]
|
||||
pub const fn data(&self) -> &crate::SolanaAccountData {
|
||||
return &self.data;
|
||||
}
|
||||
|
||||
/// Returns the account owner program public key.
|
||||
#[must_use]
|
||||
pub const fn owner(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.owner;
|
||||
}
|
||||
|
||||
/// Returns whether the account is executable.
|
||||
#[must_use]
|
||||
pub const fn executable(&self) -> bool {
|
||||
return self.executable;
|
||||
}
|
||||
|
||||
/// Returns the rent epoch reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn rent_epoch(&self) -> u64 {
|
||||
return self.rent_epoch;
|
||||
}
|
||||
|
||||
/// Returns the optional account data-space field.
|
||||
#[must_use]
|
||||
pub const fn space(&self) -> std::option::Option<u64> {
|
||||
return self.space;
|
||||
}
|
||||
|
||||
/// Decodes one account DTO 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::<WireAccount>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let owner = crate::parse_wire_pubkey(method, "owner", wire.owner.as_str());
|
||||
let owner = match owner {
|
||||
std::result::Result::Ok(owner) => owner,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let data = crate::SolanaAccountData::decode_wire(method, wire.data);
|
||||
let data = match data {
|
||||
std::result::Result::Ok(data) => data,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
lamports: wire.lamports,
|
||||
data,
|
||||
owner,
|
||||
executable: wire.executable,
|
||||
rent_epoch: wire.rent_epoch,
|
||||
space: wire.space,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// One public key plus its account returned by account-list RPC methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaKeyedAccount {
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
account: crate::SolanaAccount,
|
||||
}
|
||||
|
||||
impl SolanaKeyedAccount {
|
||||
/// Returns the account public key.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
|
||||
/// Returns the account payload.
|
||||
#[must_use]
|
||||
pub const fn account(&self) -> &crate::SolanaAccount {
|
||||
return &self.account;
|
||||
}
|
||||
|
||||
/// Decodes one keyed account 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::<WireKeyedAccount>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pubkey = crate::parse_wire_pubkey(method, "pubkey", wire.pubkey.as_str());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let account = crate::SolanaAccount::decode_wire(method, wire.account);
|
||||
let account = match account {
|
||||
std::result::Result::Ok(account) => account,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { pubkey, account });
|
||||
}
|
||||
}
|
||||
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaAccountBalance {
|
||||
address: ksp_core_lib::Pubkey,
|
||||
lamports: u64,
|
||||
}
|
||||
|
||||
impl SolanaAccountBalance {
|
||||
/// Returns the account address.
|
||||
#[must_use]
|
||||
pub const fn address(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.address;
|
||||
}
|
||||
|
||||
/// Returns the balance in lamports.
|
||||
#[must_use]
|
||||
pub const fn lamports(&self) -> u64 {
|
||||
return self.lamports;
|
||||
}
|
||||
|
||||
/// Decodes one account-balance entry 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::<WireAccountBalance>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let address = crate::parse_wire_pubkey(method, "address", wire.address.as_str());
|
||||
return match address {
|
||||
std::result::Result::Ok(address) => std::result::Result::Ok(Self { address, lamports: wire.lamports }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Result union returned by `getProgramAccounts` with or without an RPC context.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum SolanaProgramAccountsResult {
|
||||
/// Bare account list returned when `withContext` is false or absent.
|
||||
Accounts(std::vec::Vec<crate::SolanaKeyedAccount>),
|
||||
/// Contextual account list returned when `withContext` is true.
|
||||
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireAccountData {
|
||||
LegacyBinary(std::string::String),
|
||||
JsonParsed(WireParsedAccountData),
|
||||
Encoded((std::string::String, std::string::String)),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireParsedAccountData {
|
||||
program: std::string::String,
|
||||
parsed: serde_json::Value,
|
||||
space: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccount {
|
||||
lamports: u64,
|
||||
data: serde_json::Value,
|
||||
owner: std::string::String,
|
||||
executable: bool,
|
||||
#[serde(rename = "rentEpoch")]
|
||||
rent_epoch: u64,
|
||||
#[serde(default)]
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireKeyedAccount {
|
||||
pubkey: std::string::String,
|
||||
account: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccountBalance {
|
||||
address: std::string::String,
|
||||
lamports: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_accounts.rs"]
|
||||
mod tests;
|
||||
@@ -1,30 +1,7 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_canary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Commitment level accepted by the initial typed Solana HTTP canary adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
Processed,
|
||||
/// Query a bank confirmed by cluster vote.
|
||||
Confirmed,
|
||||
/// Query a finalized bank.
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl SolanaCommitment {
|
||||
/// Returns the Solana JSON-RPC commitment string.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Processed => "processed",
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Finalized => "finalized",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional typed configuration for `getBalance`.
|
||||
/// Optional typed configuration for `getBalance` retained for the `0.2.1` public canary contract.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct GetBalanceConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
@@ -55,14 +32,7 @@ impl GetBalanceConfig {
|
||||
}
|
||||
|
||||
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(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);
|
||||
return crate::SolanaContextConfig::new(self.commitment, self.min_context_slot).to_json_value();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,30 +78,6 @@ impl SolanaNodeVersion {
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed Solana RPC context used by the initial account canary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaRpcContext {
|
||||
slot: u64,
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaRpcContext {
|
||||
/// Returns the context slot reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn slot(&self) -> u64 {
|
||||
return self.slot;
|
||||
}
|
||||
|
||||
/// Returns the optional RPC API version reported by the node.
|
||||
#[must_use]
|
||||
pub fn api_version(&self) -> std::option::Option<&str> {
|
||||
return match self.api_version.as_ref() {
|
||||
std::option::Option::Some(value) => std::option::Option::Some(value.as_str()),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed lamport balance returned by the `getBalance` canary.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GetBalanceResult {
|
||||
@@ -245,10 +191,12 @@ impl crate::HttpTransportPool {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(error) => return invalid_canary_decode("getBalance", error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::GetBalanceResult {
|
||||
context: crate::SolanaRpcContext { slot: decoded.context.slot, api_version: decoded.context.api_version },
|
||||
value: decoded.value,
|
||||
});
|
||||
let context = crate::SolanaRpcContext::decode_wire("getBalance", decoded.context);
|
||||
let context = match context {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::GetBalanceResult { context, value: decoded.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,16 +208,9 @@ struct WireNodeVersion {
|
||||
feature_set: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireBalanceResult {
|
||||
context: WireRpcContext,
|
||||
context: serde_json::Value,
|
||||
value: u64,
|
||||
}
|
||||
|
||||
|
||||
577
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
Normal file
577
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
Normal file
@@ -0,0 +1,577 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
// version: 1
|
||||
|
||||
/// Contact information returned for one cluster node.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaClusterNode {
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
feature_set: std::option::Option<u32>,
|
||||
gossip: std::option::Option<std::string::String>,
|
||||
pubsub: std::option::Option<std::string::String>,
|
||||
rpc: std::option::Option<std::string::String>,
|
||||
serve_repair: std::option::Option<std::string::String>,
|
||||
shred_version: std::option::Option<u16>,
|
||||
tpu: std::option::Option<std::string::String>,
|
||||
tpu_forwards: std::option::Option<std::string::String>,
|
||||
tpu_forwards_quic: std::option::Option<std::string::String>,
|
||||
tpu_quic: std::option::Option<std::string::String>,
|
||||
tpu_vote: std::option::Option<std::string::String>,
|
||||
tvu: std::option::Option<std::string::String>,
|
||||
version: std::option::Option<std::string::String>,
|
||||
client_id: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaClusterNode {
|
||||
/// Returns the node identity public key.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.pubkey; }
|
||||
/// Returns the optional feature-set identifier.
|
||||
#[must_use]
|
||||
pub const fn feature_set(&self) -> std::option::Option<u32> { return self.feature_set; }
|
||||
/// Returns the optional gossip endpoint.
|
||||
#[must_use]
|
||||
pub fn gossip(&self) -> std::option::Option<&str> { return self.gossip.as_deref(); }
|
||||
/// Returns the optional PubSub endpoint.
|
||||
#[must_use]
|
||||
pub fn pubsub(&self) -> std::option::Option<&str> { return self.pubsub.as_deref(); }
|
||||
/// Returns the optional JSON-RPC endpoint.
|
||||
#[must_use]
|
||||
pub fn rpc(&self) -> std::option::Option<&str> { return self.rpc.as_deref(); }
|
||||
/// Returns the optional repair endpoint.
|
||||
#[must_use]
|
||||
pub fn serve_repair(&self) -> std::option::Option<&str> { return self.serve_repair.as_deref(); }
|
||||
/// Returns the optional shred version.
|
||||
#[must_use]
|
||||
pub const fn shred_version(&self) -> std::option::Option<u16> { return self.shred_version; }
|
||||
/// Returns the optional TPU endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu(&self) -> std::option::Option<&str> { return self.tpu.as_deref(); }
|
||||
/// Returns the optional TPU forwards endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_forwards(&self) -> std::option::Option<&str> { return self.tpu_forwards.as_deref(); }
|
||||
/// Returns the optional TPU forwards QUIC endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> { return self.tpu_forwards_quic.as_deref(); }
|
||||
/// Returns the optional TPU QUIC endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_quic(&self) -> std::option::Option<&str> { return self.tpu_quic.as_deref(); }
|
||||
/// Returns the optional TPU vote endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_vote(&self) -> std::option::Option<&str> { return self.tpu_vote.as_deref(); }
|
||||
/// Returns the optional TVU endpoint.
|
||||
#[must_use]
|
||||
pub fn tvu(&self) -> std::option::Option<&str> { return self.tvu.as_deref(); }
|
||||
/// Returns the optional software-version string.
|
||||
#[must_use]
|
||||
pub fn version(&self) -> std::option::Option<&str> { return self.version.as_deref(); }
|
||||
/// Returns the optional Agave client identifier extension.
|
||||
#[must_use]
|
||||
pub fn client_id(&self) -> std::option::Option<&str> { return self.client_id.as_deref(); }
|
||||
|
||||
/// Decodes one cluster-node contact 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::<WireClusterNode>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pubkey = crate::parse_wire_pubkey(method, "pubkey", wire.pubkey.as_str());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self {
|
||||
pubkey,
|
||||
feature_set: wire.feature_set,
|
||||
gossip: wire.gossip,
|
||||
pubsub: wire.pubsub,
|
||||
rpc: wire.rpc,
|
||||
serve_repair: wire.serve_repair,
|
||||
shred_version: wire.shred_version,
|
||||
tpu: wire.tpu,
|
||||
tpu_forwards: wire.tpu_forwards,
|
||||
tpu_forwards_quic: wire.tpu_forwards_quic,
|
||||
tpu_quic: wire.tpu_quic,
|
||||
tpu_vote: wire.tpu_vote,
|
||||
tvu: wire.tvu,
|
||||
version: wire.version,
|
||||
client_id: wire.client_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Epoch information returned by `getEpochInfo`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaEpochInfo {
|
||||
absolute_slot: u64,
|
||||
block_height: u64,
|
||||
epoch: u64,
|
||||
slot_index: u64,
|
||||
slots_in_epoch: u64,
|
||||
transaction_count: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaEpochInfo {
|
||||
/// Returns the absolute slot.
|
||||
#[must_use]
|
||||
pub const fn absolute_slot(&self) -> u64 { return self.absolute_slot; }
|
||||
/// Returns the block height.
|
||||
#[must_use]
|
||||
pub const fn block_height(&self) -> u64 { return self.block_height; }
|
||||
/// Returns the epoch number.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 { return self.epoch; }
|
||||
/// Returns the slot index within the epoch.
|
||||
#[must_use]
|
||||
pub const fn slot_index(&self) -> u64 { return self.slot_index; }
|
||||
/// Returns the number of slots in the epoch.
|
||||
#[must_use]
|
||||
pub const fn slots_in_epoch(&self) -> u64 { return self.slots_in_epoch; }
|
||||
/// Returns the nullable transaction count.
|
||||
#[must_use]
|
||||
pub const fn transaction_count(&self) -> std::option::Option<u64> { return self.transaction_count; }
|
||||
|
||||
/// Decodes epoch information 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::<WireEpochInfo>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
absolute_slot: wire.absolute_slot,
|
||||
block_height: wire.block_height,
|
||||
epoch: wire.epoch,
|
||||
slot_index: wire.slot_index,
|
||||
slots_in_epoch: wire.slots_in_epoch,
|
||||
transaction_count: wire.transaction_count,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Epoch schedule returned by `getEpochSchedule`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaEpochSchedule {
|
||||
first_normal_epoch: u64,
|
||||
first_normal_slot: u64,
|
||||
leader_schedule_slot_offset: u64,
|
||||
slots_per_epoch: u64,
|
||||
warmup: bool,
|
||||
}
|
||||
|
||||
impl SolanaEpochSchedule {
|
||||
/// Returns the first normal epoch.
|
||||
#[must_use]
|
||||
pub const fn first_normal_epoch(&self) -> u64 { return self.first_normal_epoch; }
|
||||
/// Returns the first normal slot.
|
||||
#[must_use]
|
||||
pub const fn first_normal_slot(&self) -> u64 { return self.first_normal_slot; }
|
||||
/// Returns the leader-schedule slot offset.
|
||||
#[must_use]
|
||||
pub const fn leader_schedule_slot_offset(&self) -> u64 { return self.leader_schedule_slot_offset; }
|
||||
/// Returns the number of slots per epoch.
|
||||
#[must_use]
|
||||
pub const fn slots_per_epoch(&self) -> u64 { return self.slots_per_epoch; }
|
||||
/// Returns whether epoch warmup is enabled.
|
||||
#[must_use]
|
||||
pub const fn warmup(&self) -> bool { return self.warmup; }
|
||||
|
||||
/// Decodes an epoch schedule 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::<WireEpochSchedule>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
first_normal_epoch: wire.first_normal_epoch,
|
||||
first_normal_slot: wire.first_normal_slot,
|
||||
leader_schedule_slot_offset: wire.leader_schedule_slot_offset,
|
||||
slots_per_epoch: wire.slots_per_epoch,
|
||||
warmup: wire.warmup,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Highest full and optional incremental snapshot slots returned by `getHighestSnapshotSlot`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaSnapshotSlotInfo {
|
||||
full: u64,
|
||||
incremental: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaSnapshotSlotInfo {
|
||||
/// Returns the highest full snapshot slot.
|
||||
#[must_use]
|
||||
pub const fn full(&self) -> u64 { return self.full; }
|
||||
/// Returns the optional highest incremental snapshot slot.
|
||||
#[must_use]
|
||||
pub const fn incremental(&self) -> std::option::Option<u64> { return self.incremental; }
|
||||
|
||||
/// Decodes snapshot-slot information 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::<WireSnapshotSlotInfo>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self { full: wire.full, incremental: wire.incremental }),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional configuration accepted by `getLeaderSchedule`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaLeaderScheduleConfig {
|
||||
identity: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
}
|
||||
|
||||
impl SolanaLeaderScheduleConfig {
|
||||
/// Creates a leader-schedule configuration.
|
||||
#[must_use]
|
||||
pub const fn new(identity: std::option::Option<ksp_core_lib::Pubkey>, commitment: std::option::Option<crate::SolanaCommitment>) -> Self {
|
||||
return Self { identity, commitment };
|
||||
}
|
||||
/// Returns the optional validator identity filter.
|
||||
#[must_use]
|
||||
pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.identity.as_ref(); }
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> { return self.commitment; }
|
||||
fn is_empty(&self) -> bool { return self.identity.is_none() && self.commitment.is_none(); }
|
||||
fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(identity) = self.identity.as_ref() {
|
||||
object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string()));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed parameter overload for `getLeaderSchedule`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaLeaderScheduleRequest {
|
||||
/// Query the current epoch, optionally with a config object.
|
||||
CurrentEpoch(std::option::Option<crate::SolanaLeaderScheduleConfig>),
|
||||
/// Query the epoch containing one slot, optionally with a config object.
|
||||
Slot {
|
||||
/// Slot whose epoch should be queried.
|
||||
slot: u64,
|
||||
/// Optional leader-schedule config sent as the second positional parameter.
|
||||
config: std::option::Option<crate::SolanaLeaderScheduleConfig>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for SolanaLeaderScheduleRequest {
|
||||
fn default() -> Self { return Self::CurrentEpoch(std::option::Option::None); }
|
||||
}
|
||||
|
||||
impl SolanaLeaderScheduleRequest {
|
||||
/// Serializes the typed overload to the exact positional JSON-RPC params.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_params(&self) -> std::vec::Vec<serde_json::Value> {
|
||||
return match self {
|
||||
Self::CurrentEpoch(std::option::Option::None) => std::vec::Vec::new(),
|
||||
Self::CurrentEpoch(std::option::Option::Some(config)) if config.is_empty() => std::vec::Vec::new(),
|
||||
Self::CurrentEpoch(std::option::Option::Some(config)) => std::vec![config.to_json_value()],
|
||||
Self::Slot { slot, config: std::option::Option::None } => std::vec![serde_json::json!(slot)],
|
||||
Self::Slot { slot, config: std::option::Option::Some(config) } if config.is_empty() => std::vec![serde_json::json!(slot)],
|
||||
Self::Slot { slot, config: std::option::Option::Some(config) } => std::vec![serde_json::json!(slot), config.to_json_value()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Leader schedule mapping validator identities to relative epoch slot indices.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaLeaderSchedule {
|
||||
entries: std::collections::BTreeMap<ksp_core_lib::Pubkey, std::vec::Vec<usize>>,
|
||||
}
|
||||
|
||||
impl SolanaLeaderSchedule {
|
||||
/// Returns the complete leader schedule map.
|
||||
#[must_use]
|
||||
pub const fn entries(&self) -> &std::collections::BTreeMap<ksp_core_lib::Pubkey, std::vec::Vec<usize>> { return &self.entries; }
|
||||
|
||||
/// Decodes a leader schedule map 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::<std::collections::BTreeMap<std::string::String, std::vec::Vec<usize>>>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
for (identity, slots) in wire {
|
||||
let pubkey = crate::parse_wire_pubkey(method, "leader_identity", identity.as_str());
|
||||
let pubkey = match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
entries.insert(pubkey, slots);
|
||||
}
|
||||
return std::result::Result::Ok(Self { entries });
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration accepted by `getVoteAccounts`.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaVoteAccountsConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
vote_pubkey: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
keep_unstaked_delinquents: std::option::Option<bool>,
|
||||
delinquent_slot_distance: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaVoteAccountsConfig {
|
||||
/// Creates a vote-accounts configuration.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
vote_pubkey: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
keep_unstaked_delinquents: std::option::Option<bool>,
|
||||
delinquent_slot_distance: std::option::Option<u64>,
|
||||
) -> Self {
|
||||
return Self { commitment, vote_pubkey, keep_unstaked_delinquents, delinquent_slot_distance };
|
||||
}
|
||||
/// Returns the optional commitment.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> { return self.commitment; }
|
||||
/// Returns the optional vote-account public key filter.
|
||||
#[must_use]
|
||||
pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.vote_pubkey.as_ref(); }
|
||||
/// Returns whether unstaked delinquent validators should be kept.
|
||||
#[must_use]
|
||||
pub const fn keep_unstaked_delinquents(&self) -> std::option::Option<bool> { return self.keep_unstaked_delinquents; }
|
||||
/// Returns the optional delinquent slot distance.
|
||||
#[must_use]
|
||||
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> { return self.delinquent_slot_distance; }
|
||||
/// 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(vote_pubkey) = self.vote_pubkey.as_ref() {
|
||||
object.insert("votePubkey".to_owned(), serde_json::Value::String(vote_pubkey.to_string()));
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.keep_unstaked_delinquents {
|
||||
object.insert("keepUnstakedDelinquents".to_owned(), serde_json::Value::Bool(value));
|
||||
}
|
||||
if let std::option::Option::Some(value) = self.delinquent_slot_distance {
|
||||
object.insert("delinquentSlotDistance".to_owned(), serde_json::Value::Number(value.into()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// One epoch-credit history entry returned by `getVoteAccounts`.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct SolanaEpochCredits {
|
||||
epoch: u64,
|
||||
credits: u64,
|
||||
previous_credits: u64,
|
||||
}
|
||||
|
||||
impl SolanaEpochCredits {
|
||||
/// Returns the epoch number.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 { return self.epoch; }
|
||||
/// Returns cumulative credits at the end of the epoch.
|
||||
#[must_use]
|
||||
pub const fn credits(&self) -> u64 { return self.credits; }
|
||||
/// Returns cumulative credits before the epoch.
|
||||
#[must_use]
|
||||
pub const fn previous_credits(&self) -> u64 { return self.previous_credits; }
|
||||
}
|
||||
|
||||
/// One validator vote-account record returned by `getVoteAccounts`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaVoteAccountInfo {
|
||||
vote_pubkey: ksp_core_lib::Pubkey,
|
||||
node_pubkey: ksp_core_lib::Pubkey,
|
||||
activated_stake: u64,
|
||||
commission: u8,
|
||||
inflation_rewards_commission_bps: std::option::Option<u16>,
|
||||
epoch_vote_account: bool,
|
||||
epoch_credits: std::vec::Vec<crate::SolanaEpochCredits>,
|
||||
last_vote: u64,
|
||||
root_slot: u64,
|
||||
}
|
||||
|
||||
impl SolanaVoteAccountInfo {
|
||||
/// Returns the vote account public key.
|
||||
#[must_use]
|
||||
pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.vote_pubkey; }
|
||||
/// Returns the validator identity public key.
|
||||
#[must_use]
|
||||
pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey { return &self.node_pubkey; }
|
||||
/// Returns the activated stake in lamports.
|
||||
#[must_use]
|
||||
pub const fn activated_stake(&self) -> u64 { return self.activated_stake; }
|
||||
/// Returns the legacy/effective percentage commission field.
|
||||
#[must_use]
|
||||
pub const fn commission(&self) -> u8 { return self.commission; }
|
||||
/// Returns the optional raw inflation-rewards commission in basis points.
|
||||
#[must_use]
|
||||
pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option<u16> { return self.inflation_rewards_commission_bps; }
|
||||
/// Returns whether the vote account is staked for the current epoch.
|
||||
#[must_use]
|
||||
pub const fn epoch_vote_account(&self) -> bool { return self.epoch_vote_account; }
|
||||
/// Returns the bounded RPC epoch-credit history.
|
||||
#[must_use]
|
||||
pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] { return self.epoch_credits.as_slice(); }
|
||||
/// Returns the latest voted slot or zero when no vote exists.
|
||||
#[must_use]
|
||||
pub const fn last_vote(&self) -> u64 { return self.last_vote; }
|
||||
/// Returns the current root slot or zero when no root exists.
|
||||
#[must_use]
|
||||
pub const fn root_slot(&self) -> u64 { return self.root_slot; }
|
||||
|
||||
/// Decodes one vote-account 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::<WireVoteAccountInfo>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let vote_pubkey = crate::parse_wire_pubkey(method, "votePubkey", wire.vote_pubkey.as_str());
|
||||
let vote_pubkey = match vote_pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let node_pubkey = crate::parse_wire_pubkey(method, "nodePubkey", wire.node_pubkey.as_str());
|
||||
let node_pubkey = match node_pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut epoch_credits = std::vec::Vec::with_capacity(wire.epoch_credits.len());
|
||||
for entry in wire.epoch_credits {
|
||||
epoch_credits.push(crate::SolanaEpochCredits { epoch: entry[0], credits: entry[1], previous_credits: entry[2] });
|
||||
}
|
||||
return std::result::Result::Ok(Self {
|
||||
vote_pubkey,
|
||||
node_pubkey,
|
||||
activated_stake: wire.activated_stake,
|
||||
commission: wire.commission,
|
||||
inflation_rewards_commission_bps: wire.inflation_rewards_commission_bps,
|
||||
epoch_vote_account: wire.epoch_vote_account,
|
||||
epoch_credits,
|
||||
last_vote: wire.last_vote,
|
||||
root_slot: wire.root_slot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Current and delinquent validator vote-account groups returned by `getVoteAccounts`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaVoteAccountStatus {
|
||||
current: std::vec::Vec<crate::SolanaVoteAccountInfo>,
|
||||
delinquent: std::vec::Vec<crate::SolanaVoteAccountInfo>,
|
||||
}
|
||||
|
||||
impl SolanaVoteAccountStatus {
|
||||
/// Returns current vote accounts.
|
||||
#[must_use]
|
||||
pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] { return self.current.as_slice(); }
|
||||
/// Returns delinquent vote accounts.
|
||||
#[must_use]
|
||||
pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] { return self.delinquent.as_slice(); }
|
||||
|
||||
/// Decodes the complete vote-account status response 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::<WireVoteAccountStatus>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut current = std::vec::Vec::with_capacity(wire.current.len());
|
||||
for value in wire.current {
|
||||
let decoded = crate::SolanaVoteAccountInfo::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(info) => current.push(info),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
let mut delinquent = std::vec::Vec::with_capacity(wire.delinquent.len());
|
||||
for value in wire.delinquent {
|
||||
let decoded = crate::SolanaVoteAccountInfo::decode_wire(method, value);
|
||||
match decoded {
|
||||
std::result::Result::Ok(info) => delinquent.push(info),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(Self { current, delinquent });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireClusterNode {
|
||||
pubkey: std::string::String,
|
||||
#[serde(default)] feature_set: std::option::Option<u32>,
|
||||
#[serde(default)] gossip: std::option::Option<std::string::String>,
|
||||
#[serde(default)] pubsub: std::option::Option<std::string::String>,
|
||||
#[serde(default)] rpc: std::option::Option<std::string::String>,
|
||||
#[serde(default)] serve_repair: std::option::Option<std::string::String>,
|
||||
#[serde(default)] shred_version: std::option::Option<u16>,
|
||||
#[serde(default)] tpu: std::option::Option<std::string::String>,
|
||||
#[serde(default)] tpu_forwards: std::option::Option<std::string::String>,
|
||||
#[serde(default)] tpu_forwards_quic: std::option::Option<std::string::String>,
|
||||
#[serde(default)] tpu_quic: std::option::Option<std::string::String>,
|
||||
#[serde(default)] tpu_vote: std::option::Option<std::string::String>,
|
||||
#[serde(default)] tvu: std::option::Option<std::string::String>,
|
||||
#[serde(default)] version: std::option::Option<std::string::String>,
|
||||
#[serde(default)] client_id: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireEpochInfo {
|
||||
absolute_slot: u64,
|
||||
block_height: u64,
|
||||
epoch: u64,
|
||||
slot_index: u64,
|
||||
slots_in_epoch: u64,
|
||||
transaction_count: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireEpochSchedule {
|
||||
first_normal_epoch: u64,
|
||||
first_normal_slot: u64,
|
||||
leader_schedule_slot_offset: u64,
|
||||
slots_per_epoch: u64,
|
||||
warmup: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireSnapshotSlotInfo {
|
||||
full: u64,
|
||||
incremental: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireVoteAccountInfo {
|
||||
vote_pubkey: std::string::String,
|
||||
node_pubkey: std::string::String,
|
||||
activated_stake: u64,
|
||||
commission: u8,
|
||||
#[serde(default)]
|
||||
inflation_rewards_commission_bps: std::option::Option<u16>,
|
||||
epoch_vote_account: bool,
|
||||
epoch_credits: std::vec::Vec<[u64; 3]>,
|
||||
last_vote: u64,
|
||||
root_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireVoteAccountStatus {
|
||||
current: std::vec::Vec<serde_json::Value>,
|
||||
delinquent: std::vec::Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_cluster.rs"]
|
||||
mod tests;
|
||||
205
crates/ksp-onchain-transport-lib/src/rpc_common.rs
Normal file
205
crates/ksp-onchain-transport-lib/src/rpc_common.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
// version: 1
|
||||
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SolanaCommitment {
|
||||
/// Query the most recent processed bank.
|
||||
Processed,
|
||||
/// Query a bank confirmed by cluster vote.
|
||||
Confirmed,
|
||||
/// Query a finalized bank.
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl SolanaCommitment {
|
||||
/// Returns the Solana JSON-RPC commitment string.
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Processed => "processed",
|
||||
Self::Confirmed => "confirmed",
|
||||
Self::Finalized => "finalized",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional commitment-only configuration shared by typed Solana HTTP RPC methods.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaCommitmentConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
}
|
||||
|
||||
impl SolanaCommitmentConfig {
|
||||
/// Creates an explicit commitment-only configuration.
|
||||
#[must_use]
|
||||
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>) -> Self {
|
||||
return Self { commitment };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns whether this config serializes to an empty JSON object.
|
||||
#[must_use]
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return 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(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
return serde_json::Value::Object(object);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional commitment and minimum-context configuration shared by typed Solana HTTP RPC methods.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct SolanaContextConfig {
|
||||
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl SolanaContextConfig {
|
||||
/// Creates an explicit context-aware RPC configuration.
|
||||
#[must_use]
|
||||
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>, min_context_slot: std::option::Option<u64>) -> Self {
|
||||
return Self { commitment, min_context_slot };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// 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 this config serializes to an empty JSON object.
|
||||
#[must_use]
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.commitment.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(commitment) = self.commitment {
|
||||
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed Solana RPC context shared by contextual HTTP responses.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaRpcContext {
|
||||
slot: u64,
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl SolanaRpcContext {
|
||||
/// Returns the context slot reported by the RPC node.
|
||||
#[must_use]
|
||||
pub const fn slot(&self) -> u64 {
|
||||
return self.slot;
|
||||
}
|
||||
|
||||
/// Returns the optional RPC API version reported by the node.
|
||||
#[must_use]
|
||||
pub fn api_version(&self) -> std::option::Option<&str> {
|
||||
return match self.api_version.as_ref() {
|
||||
std::option::Option::Some(value) => std::option::Option::Some(value.as_str()),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Decodes one RPC context from a parsed JSON value.
|
||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||
let decoded = crate::decode_wire_json::<WireRpcContext>(method, value);
|
||||
let context = match decoded {
|
||||
std::result::Result::Ok(context) => context,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { slot: context.slot, api_version: context.api_version });
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaRpcResponse<T> {
|
||||
context: crate::SolanaRpcContext,
|
||||
value: T,
|
||||
}
|
||||
|
||||
impl<T> SolanaRpcResponse<T> {
|
||||
/// Returns the Solana response context.
|
||||
#[must_use]
|
||||
pub const fn context(&self) -> &crate::SolanaRpcContext {
|
||||
return &self.context;
|
||||
}
|
||||
|
||||
/// Returns the typed response value.
|
||||
#[must_use]
|
||||
pub const fn value(&self) -> &T {
|
||||
return &self.value;
|
||||
}
|
||||
|
||||
/// Creates a contextual response after wire decoding and validation.
|
||||
#[must_use]
|
||||
pub(crate) const fn new(context: crate::SolanaRpcContext, value: T) -> Self {
|
||||
return Self { context, value };
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes one private serde wire type and maps shape failures to the shared Transport error domain.
|
||||
pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<T> {
|
||||
let decoded = serde_json::from_value::<T>(value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(decoded) => std::result::Result::Ok(decoded),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP response has an invalid wire shape")
|
||||
.with_context("rpc_method", method)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Parses a base58 public key from one wire field without echoing its value into diagnostics.
|
||||
pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_core_lib::Result<ksp_core_lib::Pubkey> {
|
||||
let parsed = value.parse::<ksp_core_lib::Pubkey>();
|
||||
return match parsed {
|
||||
std::result::Result::Ok(pubkey) => std::result::Result::Ok(pubkey),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Solana HTTP response contains an invalid public key")
|
||||
.with_context("rpc_method", method)
|
||||
.with_context("field", field),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_common.rs"]
|
||||
mod tests;
|
||||
138
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
Normal file
138
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
// version: 1
|
||||
|
||||
/// Exclusive selector accepted by token-account list RPC methods.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum SolanaTokenAccountSelector {
|
||||
/// Select token accounts for one mint.
|
||||
Mint(ksp_core_lib::Pubkey),
|
||||
/// Select token accounts owned by one token program.
|
||||
ProgramId(ksp_core_lib::Pubkey),
|
||||
}
|
||||
|
||||
impl SolanaTokenAccountSelector {
|
||||
/// Serializes the exclusive selector to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
return match self {
|
||||
Self::Mint(pubkey) => serde_json::json!({"mint": pubkey.to_string()}),
|
||||
Self::ProgramId(pubkey) => serde_json::json!({"programId": pubkey.to_string()}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Token amount returned by Solana HTTP token RPC methods.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaTokenAmount {
|
||||
amount: std::string::String,
|
||||
decimals: u8,
|
||||
ui_amount: std::option::Option<f64>,
|
||||
ui_amount_string: std::string::String,
|
||||
}
|
||||
|
||||
impl SolanaTokenAmount {
|
||||
/// Returns the integer token amount as an exact decimal string.
|
||||
#[must_use]
|
||||
pub fn amount(&self) -> &str {
|
||||
return self.amount.as_str();
|
||||
}
|
||||
|
||||
/// Returns the mint decimal precision.
|
||||
#[must_use]
|
||||
pub const fn decimals(&self) -> u8 {
|
||||
return self.decimals;
|
||||
}
|
||||
|
||||
/// Returns the nullable floating-point UI amount exactly as provided by RPC.
|
||||
#[must_use]
|
||||
pub const fn ui_amount(&self) -> std::option::Option<f64> {
|
||||
return self.ui_amount;
|
||||
}
|
||||
|
||||
/// Returns the exact UI amount string provided by RPC.
|
||||
#[must_use]
|
||||
pub fn ui_amount_string(&self) -> &str {
|
||||
return self.ui_amount_string.as_str();
|
||||
}
|
||||
|
||||
/// Decodes one token amount 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::<WireTokenAmount>(method, value);
|
||||
return match decoded {
|
||||
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
|
||||
amount: wire.amount,
|
||||
decimals: wire.decimals,
|
||||
ui_amount: wire.ui_amount,
|
||||
ui_amount_string: wire.ui_amount_string,
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Token-account balance entry returned by `getTokenLargestAccounts`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SolanaTokenAccountBalance {
|
||||
address: ksp_core_lib::Pubkey,
|
||||
amount: crate::SolanaTokenAmount,
|
||||
}
|
||||
|
||||
impl SolanaTokenAccountBalance {
|
||||
/// Returns the token account address.
|
||||
#[must_use]
|
||||
pub const fn address(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.address;
|
||||
}
|
||||
|
||||
/// Returns the token amount fields.
|
||||
#[must_use]
|
||||
pub const fn amount(&self) -> &crate::SolanaTokenAmount {
|
||||
return &self.amount;
|
||||
}
|
||||
|
||||
/// Decodes one token-account balance entry 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::<WireTokenAccountBalance>(method, value);
|
||||
let wire = match decoded {
|
||||
std::result::Result::Ok(wire) => wire,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let address = crate::parse_wire_pubkey(method, "address", wire.address.as_str());
|
||||
let address = match address {
|
||||
std::result::Result::Ok(address) => address,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let amount = crate::SolanaTokenAmount {
|
||||
amount: wire.amount,
|
||||
decimals: wire.decimals,
|
||||
ui_amount: wire.ui_amount,
|
||||
ui_amount_string: wire.ui_amount_string,
|
||||
};
|
||||
return std::result::Result::Ok(Self { address, amount });
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireTokenAmount {
|
||||
amount: std::string::String,
|
||||
decimals: u8,
|
||||
#[serde(rename = "uiAmount")]
|
||||
ui_amount: std::option::Option<f64>,
|
||||
#[serde(rename = "uiAmountString")]
|
||||
ui_amount_string: std::string::String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireTokenAccountBalance {
|
||||
address: std::string::String,
|
||||
amount: std::string::String,
|
||||
decimals: u8,
|
||||
#[serde(rename = "uiAmount")]
|
||||
ui_amount: std::option::Option<f64>,
|
||||
#[serde(rename = "uiAmountString")]
|
||||
ui_amount_string: std::string::String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_tokens.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -171,3 +171,31 @@ fn public_typed_canary_contracts_are_available_from_crate_root() {
|
||||
assert_eq!(config.min_context_slot(), std::option::Option::Some(42));
|
||||
assert_eq!(ksp_onchain_transport_lib::SolanaNodeHealth::Healthy, ksp_onchain_transport_lib::SolanaNodeHealth::Healthy);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn public_pre_002_shared_rpc_types_are_constructible_from_crate_root() {
|
||||
let context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(123),
|
||||
);
|
||||
assert_eq!(context.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized));
|
||||
assert_eq!(context.min_context_slot(), std::option::Option::Some(123));
|
||||
let account = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::JsonParsed),
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(account.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::JsonParsed));
|
||||
let pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let selector = ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(pubkey);
|
||||
assert!(matches!(selector, ksp_onchain_transport_lib::SolanaTokenAccountSelector::Mint(_)));
|
||||
let vote = ksp_onchain_transport_lib::SolanaVoteAccountsConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(pubkey),
|
||||
std::option::Option::Some(true),
|
||||
std::option::Option::Some(128),
|
||||
);
|
||||
assert_eq!(vote.vote_pubkey(), std::option::Option::Some(&pubkey));
|
||||
}
|
||||
|
||||
53
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
Normal file
53
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn account_config_serializes_all_common_fields() {
|
||||
let config = crate::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::Some(crate::SolanaDataSliceConfig::new(8, 32)),
|
||||
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||
std::option::Option::Some(99),
|
||||
);
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({"encoding":"base64","dataSlice":{"offset":8,"length":32},"commitment":"finalized","minContextSlot":99})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn program_accounts_config_preserves_filter_variants_and_flags() {
|
||||
let filters = std::vec![
|
||||
crate::SolanaProgramAccountFilter::DataSize(165),
|
||||
crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(4, crate::SolanaMemcmpBytes::Base64("AQID".to_owned()))),
|
||||
crate::SolanaProgramAccountFilter::TokenAccountState,
|
||||
];
|
||||
let config = crate::SolanaProgramAccountsConfig::new(crate::SolanaAccountInfoConfig::default(), filters, std::option::Option::Some(true), std::option::Option::Some(true));
|
||||
assert_eq!(
|
||||
config.to_json_value(),
|
||||
serde_json::json!({
|
||||
"filters":[{"dataSize":165},{"memcmp":{"offset":4,"bytes":"AQID","encoding":"base64"}},"tokenAccountState"],
|
||||
"withContext":true,
|
||||
"sortResults":true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_wire_fixture_preserves_legacy_encoded_and_json_parsed_data() {
|
||||
let values: std::vec::Vec<serde_json::Value> = serde_json::from_str(include_str!("../fixtures/http/account_data.variants.json")).expect("fixture must decode");
|
||||
let legacy = crate::SolanaAccount::decode_wire("fixture", values[0].clone()).expect("legacy account must decode");
|
||||
assert!(matches!(legacy.data(), crate::SolanaAccountData::LegacyBinary(_)));
|
||||
assert_eq!(legacy.space(), std::option::Option::None);
|
||||
let encoded = crate::SolanaAccount::decode_wire("fixture", values[1].clone()).expect("encoded account must decode");
|
||||
assert!(matches!(encoded.data(), crate::SolanaAccountData::Encoded { encoding: crate::SolanaAccountEncoding::Base64Zstd, .. }));
|
||||
let parsed = crate::SolanaAccount::decode_wire("fixture", values[2].clone()).expect("parsed account must decode");
|
||||
match parsed.data() {
|
||||
crate::SolanaAccountData::JsonParsed(value) => {
|
||||
assert_eq!(value.program(), "spl-token");
|
||||
assert_eq!(value.space(), 165);
|
||||
assert_eq!(value.parsed()["type"], serde_json::json!("account"));
|
||||
}
|
||||
_ => assert!(false, "jsonParsed fixture must retain parsed data"),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_canary.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn pool_for_url(url: &str) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
|
||||
52
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
Normal file
52
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() {
|
||||
let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/cluster_node.v4_2_1.json")).expect("fixture must decode");
|
||||
let node = crate::SolanaClusterNode::decode_wire("getClusterNodes", value).expect("cluster node must decode");
|
||||
assert_eq!(node.client_id(), std::option::Option::Some("Agave"));
|
||||
assert_eq!(node.rpc(), std::option::Option::Some("127.0.0.1:8899"));
|
||||
assert_eq!(node.pubsub(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_account_fixture_preserves_optional_basis_point_commission_and_epoch_credit_triples() {
|
||||
let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/vote_account.v4_2_1.json")).expect("fixture must decode");
|
||||
let info = crate::SolanaVoteAccountInfo::decode_wire("getVoteAccounts", value).expect("vote account must decode");
|
||||
assert_eq!(info.commission(), 8);
|
||||
assert_eq!(info.inflation_rewards_commission_bps(), std::option::Option::Some(750));
|
||||
assert_eq!(info.epoch_credits().len(), 2);
|
||||
assert_eq!(info.epoch_credits()[0].epoch(), 700);
|
||||
assert_eq!(info.epoch_credits()[0].previous_credits(), 90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vote_account_wire_accepts_nodes_predating_basis_point_commission_field() {
|
||||
let value = serde_json::json!({
|
||||
"votePubkey":"11111111111111111111111111111111",
|
||||
"nodePubkey":"11111111111111111111111111111111",
|
||||
"activatedStake":1,
|
||||
"commission":5,
|
||||
"epochVoteAccount":true,
|
||||
"epochCredits":[],
|
||||
"lastVote":2,
|
||||
"rootSlot":1
|
||||
});
|
||||
let info = crate::SolanaVoteAccountInfo::decode_wire("getVoteAccounts", value).expect("legacy node shape must decode");
|
||||
assert_eq!(info.inflation_rewards_commission_bps(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leader_schedule_request_encodes_current_epoch_and_slot_overloads_without_ambiguous_arrays() {
|
||||
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let config = crate::SolanaLeaderScheduleConfig::new(std::option::Option::Some(identity), std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
assert_eq!(
|
||||
crate::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::Some(config.clone())).to_json_params(),
|
||||
std::vec![serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})]
|
||||
);
|
||||
assert_eq!(
|
||||
crate::SolanaLeaderScheduleRequest::Slot { slot: 123, config: std::option::Option::Some(config) }.to_json_params(),
|
||||
std::vec![serde_json::json!(123), serde_json::json!({"identity":"11111111111111111111111111111111","commitment":"finalized"})]
|
||||
);
|
||||
}
|
||||
19
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
Normal file
19
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn shared_context_configs_preserve_commitment_and_min_context_slot() {
|
||||
let commitment = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||
assert_eq!(commitment.to_json_value(), serde_json::json!({"commitment":"finalized"}));
|
||||
let context = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(42));
|
||||
assert_eq!(context.to_json_value(), serde_json::json!({"commitment":"confirmed","minContextSlot":42}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_context_preserves_nullable_api_version() {
|
||||
let with_version = crate::SolanaRpcContext::decode_wire("fixture", serde_json::json!({"slot":10,"apiVersion":"4.2.1"})).expect("context must decode");
|
||||
assert_eq!(with_version.slot(), 10);
|
||||
assert_eq!(with_version.api_version(), std::option::Option::Some("4.2.1"));
|
||||
let without_version = crate::SolanaRpcContext::decode_wire("fixture", serde_json::json!({"slot":11})).expect("context must decode");
|
||||
assert_eq!(without_version.api_version(), std::option::Option::None);
|
||||
}
|
||||
19
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
Normal file
19
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn token_selector_is_exclusive_by_construction() {
|
||||
let mint = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
assert_eq!(crate::SolanaTokenAccountSelector::Mint(mint).to_json_value(), serde_json::json!({"mint":"11111111111111111111111111111111"}));
|
||||
assert_eq!(crate::SolanaTokenAccountSelector::ProgramId(mint).to_json_value(), serde_json::json!({"programId":"11111111111111111111111111111111"}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_amount_fixture_preserves_nullable_ui_amount() {
|
||||
let value: serde_json::Value = serde_json::from_str(include_str!("../fixtures/http/token_amount.null_ui.json")).expect("fixture must decode");
|
||||
let amount = crate::SolanaTokenAmount::decode_wire("fixture", value).expect("token amount must decode");
|
||||
assert_eq!(amount.amount(), "18446744073709551615");
|
||||
assert_eq!(amount.decimals(), 9);
|
||||
assert_eq!(amount.ui_amount(), std::option::Option::None);
|
||||
assert_eq!(amount.ui_amount_string(), "18446744073.709551615");
|
||||
}
|
||||
227
deltas/0.2.2/pre.002.md
Normal file
227
deltas/0.2.2/pre.002.md
Normal file
@@ -0,0 +1,227 @@
|
||||
<!-- file: deltas/0.2.2/pre.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.2-pre.002` — primitives RPC Accounts/Tokens/Cluster et wire commun
|
||||
|
||||
## Base requise
|
||||
|
||||
Livraison précédente corrigée :
|
||||
|
||||
```text
|
||||
0.2.2-pre.001-fix.001
|
||||
workspace.package.version = "0.2.2-pre.1"
|
||||
```
|
||||
|
||||
Le plan canonique attendu est `docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md` version 3, qui inclut le réaudit Agave `v4.2.1`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Installer les primitives/configurations/résultats partagés nécessaires aux 22 wrappers `0.2.2`, sans encore implémenter les cinq wrappers
|
||||
Accounts prévus pour `pre.003`. La tranche mutualise les contrats nés avec les canaris `0.2.1`, fixe les formes wire Account/Token/Cluster et
|
||||
ajoute des fixtures déterministes pour les variantes nullable/versionnées identifiées pendant `pre.001` et son fix.
|
||||
|
||||
## Version Cargo
|
||||
|
||||
Conformément à `VER-ID-009`, la nouvelle prerelease synchronise le signal technique :
|
||||
|
||||
```text
|
||||
0.2.2-pre.1 -> 0.2.2-pre.2
|
||||
```
|
||||
|
||||
Aucune dépendance ou feature Cargo n'est ajoutée.
|
||||
|
||||
## Implémentation
|
||||
|
||||
### RPC commun
|
||||
|
||||
Un nouveau module commun possède désormais :
|
||||
|
||||
```text
|
||||
SolanaCommitment
|
||||
SolanaCommitmentConfig
|
||||
SolanaContextConfig
|
||||
SolanaRpcContext
|
||||
SolanaRpcResponse<T>
|
||||
```
|
||||
|
||||
`SolanaCommitment` et `SolanaRpcContext` ont été déplacés hors du module canari sans casser leurs réexports crate-root. `GetBalanceConfig`
|
||||
conserve son nom, ses champs internes, son constructeur, ses getters et sa forme `Debug` `0.2.1`; seule sa sérialisation réutilise désormais
|
||||
`SolanaContextConfig`.
|
||||
|
||||
Les helpers internes de décodage :
|
||||
|
||||
- convertissent les erreurs serde vers `ERROR_CODE_INVALID_RESPONSE` ;
|
||||
- convertissent explicitement les chaînes de public key vers `ksp_core_lib::Pubkey` ;
|
||||
- n'incluent pas la valeur de public key invalide dans les diagnostics.
|
||||
|
||||
### Accounts
|
||||
|
||||
Les primitives suivantes sont ajoutées :
|
||||
|
||||
```text
|
||||
SolanaAccountEncoding
|
||||
SolanaDataSliceConfig
|
||||
SolanaAccountInfoConfig
|
||||
SolanaLargestAccountsFilter
|
||||
SolanaLargestAccountsConfig
|
||||
SolanaMemcmpBytes
|
||||
SolanaMemcmpFilter
|
||||
SolanaProgramAccountFilter
|
||||
SolanaProgramAccountsConfig
|
||||
SolanaParsedAccountData
|
||||
SolanaAccountData
|
||||
SolanaAccount
|
||||
SolanaKeyedAccount
|
||||
SolanaAccountBalance
|
||||
SolanaProgramAccountsResult
|
||||
```
|
||||
|
||||
`SolanaAccountData` préserve les trois familles de wire : chaîne legacy, tuple encodé et `jsonParsed`. Le contenu `parsed` reste un
|
||||
`serde_json::Value`; aucun décodage Program/SPL n'entre dans Transport. `space` reste `Option<u64>`.
|
||||
|
||||
`SolanaProgramAccountFilter` conserve `dataSize`, `memcmp` et `tokenAccountState`. Les bytes `memcmp` peuvent rester base58/base64 textuels ou
|
||||
raw sans ajouter `bs58`/`base64`; les limites de cardinalité et de taille seront appliquées par les wrappers/validation `pre.003` conformément au
|
||||
plan.
|
||||
|
||||
### Tokens
|
||||
|
||||
Les primitives suivantes sont ajoutées :
|
||||
|
||||
```text
|
||||
SolanaTokenAccountSelector
|
||||
SolanaTokenAmount
|
||||
SolanaTokenAccountBalance
|
||||
```
|
||||
|
||||
Le selector est un enum `Mint | ProgramId`, ce qui interdit par construction un objet contenant simultanément les deux clés. `uiAmount` reste
|
||||
`Option<f64>` et `amount`/`uiAmountString` restent des chaînes exactes du wire.
|
||||
|
||||
### Cluster
|
||||
|
||||
Les DTOs/configs partagés suivants sont ajoutés :
|
||||
|
||||
```text
|
||||
SolanaClusterNode
|
||||
SolanaEpochInfo
|
||||
SolanaEpochSchedule
|
||||
SolanaSnapshotSlotInfo
|
||||
SolanaLeaderScheduleConfig
|
||||
SolanaLeaderScheduleRequest
|
||||
SolanaLeaderSchedule
|
||||
SolanaVoteAccountsConfig
|
||||
SolanaEpochCredits
|
||||
SolanaVoteAccountInfo
|
||||
SolanaVoteAccountStatus
|
||||
```
|
||||
|
||||
Le DTO node conserve tous les endpoints comme chaînes optionnelles et inclut `clientId: Option<String>` confirmé dans Agave `v4.2.1`.
|
||||
`SolanaVoteAccountInfo` inclut `inflationRewardsCommissionBps: Option<u16>` sans le dériver de `commission`, ainsi que les triples epoch credits
|
||||
sous un petit DTO nommé. `SolanaLeaderScheduleRequest` encode explicitement les overloads current-epoch/config et slot/config.
|
||||
|
||||
## Fixtures déterministes ajoutées
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/account_data.variants.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/token_amount.null_ui.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/cluster_node.v4_2_1.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/vote_account.v4_2_1.json
|
||||
```
|
||||
|
||||
Elles couvrent notamment legacy/encoded/jsonParsed Account data, `space: null`, `uiAmount: null`, `clientId`, champs Cluster absents et présence
|
||||
de `inflationRewardsCommissionBps`. Un test supplémentaire vérifie l'absence de ce dernier champ pour une forme de noeud plus ancienne.
|
||||
|
||||
## Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_common.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_tokens.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/account_data.variants.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/token_amount.null_ui.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/cluster_node.v4_2_1.json
|
||||
crates/ksp-onchain-transport-lib/fixtures/http/vote_account.v4_2_1.json
|
||||
deltas/0.2.2/pre.002.md
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_canary.rs
|
||||
crates/ksp-onchain-transport-lib/unit_tests/rpc_canary.rs
|
||||
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
```
|
||||
|
||||
## Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## Fichiers volontairement inchangés
|
||||
|
||||
```text
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md
|
||||
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||
crates/ksp-onchain-transport-lib/src/executor.rs
|
||||
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
crates/ksp-config-lib/**
|
||||
config/**
|
||||
```
|
||||
|
||||
Le plan ne change pas : `pre.003` reste consacré aux cinq wrappers Accounts.
|
||||
|
||||
## Validations exécutées
|
||||
|
||||
- reconstruction locale de l'état `v0.2.1 + pre.001 + pre.001-fix.001` à partir des archives fournies ;
|
||||
- relecture de `RULES_RUST.md`, `FILE_CONTRACTS.md`, `VERSION_WORKFLOW.md` et du plan `009` version 3 ;
|
||||
- recoupement des structures account/token Agave `v4.2.1` (`UiAccount`, `UiAccountData`, `UiAccountEncoding`, `UiTokenAmount`) ;
|
||||
- recoupement des extensions Cluster/Vote Agave `v4.2.1` (`client_id`, `inflation_rewards_commission_bps`) ;
|
||||
- contrôle statique des réexports crate-root et de l'absence de nouvelle dépendance Cargo ;
|
||||
- contrôle statique de l'absence de wrapper `0.2.2` ajouté prématurément dans cette tranche ;
|
||||
- contrôle des fixtures JSON avec un parseur JSON local ;
|
||||
- contrôle du contenu de l'archive d'échange après génération.
|
||||
|
||||
## Validations non exécutées
|
||||
|
||||
Le sandbox ne fournit pas `cargo` ni `rustfmt`. Les commandes suivantes n'ont donc pas été déclarées comme réussies et doivent être exécutées
|
||||
sur le checkout de développement avant commit :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
```
|
||||
|
||||
Conformément à `RUST-*`, `cargo test --workspace` reste requis au point de contrôle de session/clôture approprié. Aucun `cargo tree` supplémentaire
|
||||
n'est requis par ce delta, puisqu'aucune dépendance ni feature n'a changé.
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- conserver les quatre canaris `0.2.1` et leurs noms publics ;
|
||||
- centraliser les primitives réellement communes plutôt que créer un mega DTO ;
|
||||
- utiliser `ksp_core_lib::Pubkey` dans les DTOs publics, avec conversion wire explicite ;
|
||||
- ne pas activer `serde` sur `solana-pubkey` uniquement pour les réponses RPC ;
|
||||
- ne pas ajouter `base64`, `bs58`, SPL ou client RPC haut niveau ;
|
||||
- conserver les champs provider/versionnés optionnels au lieu de les rendre obligatoires ;
|
||||
- ne pas ajouter de logs par DTO : l'observabilité reste au niveau du transport/executor.
|
||||
|
||||
## Questions ouvertes
|
||||
|
||||
Aucune question bloquante pour `pre.003`.
|
||||
|
||||
Le formatage final exact doit être produit par le `rustfmt` canonique du dépôt lors de l'application locale, puisque le binaire n'est pas disponible
|
||||
dans le sandbox d'échange.
|
||||
|
||||
## Suite
|
||||
|
||||
`0.2.2-pre.003` : implémenter les cinq wrappers Accounts (`getAccountInfo`, `getLargestAccounts`, `getMinimumBalanceForRentExemption`,
|
||||
`getMultipleAccounts`, `getProgramAccounts`) avec validation de cardinalité/filtres, décodage via les DTOs de cette tranche, serveur HTTP local et
|
||||
fixtures déterministes.
|
||||
Reference in New Issue
Block a user