v0.2.4-pre.002

This commit is contained in:
2026-08-18 20:12:46 +02:00
parent cb19742cf7
commit cf8d79567d
19 changed files with 1869 additions and 8 deletions

View File

@@ -6,7 +6,7 @@ resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
[workspace.package]
version = "0.2.4-pre.1"
version = "0.2.4-pre.2"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,4 @@
[
{"commitment":[1,2,3,4],"totalStake":1000000000},
{"commitment":null,"totalStake":1000000000}
]

View File

@@ -0,0 +1,7 @@
{
"byIdentity": {
"11111111111111111111111111111111": [8,7],
"Vote111111111111111111111111111111111111111": [4,4]
},
"range": {"firstSlot":430000000,"lastSlot":430000099}
}

View File

@@ -0,0 +1,20 @@
[
{
"previousBlockhash":"previous-a",
"blockhash":"block-a",
"parentSlot":1,
"blockTime":null,
"blockHeight":null
},
{
"previousBlockhash":"previous-b",
"blockhash":"block-b",
"parentSlot":2,
"transactions":null,
"signatures":null,
"rewards":null,
"numRewardPartitions":null,
"blockTime":null,
"blockHeight":null
}
]

View File

@@ -0,0 +1,36 @@
{
"previousBlockhash":"previous-blockhash-fixture",
"blockhash":"blockhash-fixture",
"parentSlot":430000122,
"transactions":[
{
"transaction":{"signatures":["signature-fixture"],"message":{"accountKeys":[],"recentBlockhash":"recent-fixture","instructions":[]}},
"meta":{"err":null,"fee":5000},
"version":1
},
{
"transaction":["AQID","base64"],
"meta":null
}
],
"rewards":[
{
"pubkey":"11111111111111111111111111111111",
"lamports":2500,
"postBalance":1000002500,
"rewardType":"Fee",
"commission":null,
"commissionBps":1234
},
{
"pubkey":"Vote111111111111111111111111111111111111111",
"lamports":1250,
"postBalance":2000001250,
"rewardType":"Voting",
"commission":5
}
],
"numRewardPartitions":4,
"blockTime":1787072400,
"blockHeight":410000000
}

View File

@@ -0,0 +1 @@
{"initial":0.08,"terminal":0.015,"taper":0.15,"foundation":0.05,"foundationTerm":7.0}

View File

@@ -0,0 +1 @@
{"total":0.042,"validator":0.041,"foundation":0.001,"epoch":812}

View File

@@ -0,0 +1,5 @@
[
{"epoch":811,"effectiveSlot":429900000,"amount":2500000,"postBalance":1002500000,"commission":7,"commissionBps":725},
null,
{"epoch":811,"effectiveSlot":429900000,"amount":1250000,"postBalance":501250000,"commission":null}
]

View File

@@ -0,0 +1,4 @@
[
{"slot":430000123,"numTransactions":250000,"numNonVoteTransactions":175000,"numSlots":120,"samplePeriodSecs":60},
{"slot":430000003,"numTransactions":240000,"numSlots":118,"samplePeriodSecs":60}
]

View File

@@ -0,0 +1,9 @@
{
"total":600000000000000000,
"circulating":500000000000000000,
"nonCirculating":100000000000000000,
"nonCirculatingAccounts":[
"11111111111111111111111111111111",
"Vote111111111111111111111111111111111111111"
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 16
// version: 17
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -12,7 +12,8 @@
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through
//! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete
//! modern/legacy `getTransaction` coverage. The `0.2.4` family remains staged.
//! modern/legacy `getTransaction` coverage. `0.2.4-pre.002` adds the shared Blocks/Economics wire, config and result primitives without yet advancing
//! any `V0_2_4` method to a typed wrapper.
mod client;
mod constants;
@@ -22,9 +23,11 @@ mod json_rpc;
mod pool;
mod resilience;
mod rpc_accounts;
mod rpc_blocks;
mod rpc_canary;
mod rpc_cluster;
mod rpc_common;
mod rpc_economics;
mod rpc_method;
mod rpc_tokens;
mod rpc_transactions;
@@ -126,6 +129,28 @@ pub use self::rpc_accounts::SolanaProgramAccountFilter;
pub use self::rpc_accounts::SolanaProgramAccountsConfig;
/// Result union returned by `getProgramAccounts` with or without an RPC context.
pub use self::rpc_accounts::SolanaProgramAccountsResult;
/// Commitment distribution returned by `getBlockCommitment`.
pub use self::rpc_blocks::SolanaBlockCommitment;
/// Block-production counts returned by `getBlockProduction` before the shared RPC context wrapper is applied.
pub use self::rpc_blocks::SolanaBlockProduction;
/// Optional configuration accepted by `getBlockProduction`.
pub use self::rpc_blocks::SolanaBlockProductionConfig;
/// Slot range accepted inside `getBlockProduction` configuration.
pub use self::rpc_blocks::SolanaBlockProductionRange;
/// Effective range reported inside `getBlockProduction` results.
pub use self::rpc_blocks::SolanaBlockProductionResultRange;
/// One reward entry returned in a confirmed block.
pub use self::rpc_blocks::SolanaBlockReward;
/// One transaction entry returned inside a confirmed block.
pub use self::rpc_blocks::SolanaBlockTransaction;
/// Confirmed block wire result returned by `getBlock` when the RPC result is non-null.
pub use self::rpc_blocks::SolanaConfirmedBlock;
/// Modern configuration object accepted by `getBlock`.
pub use self::rpc_blocks::SolanaGetBlockConfig;
/// One entry returned by `getRecentPerformanceSamples`.
pub use self::rpc_blocks::SolanaPerformanceSample;
/// Transaction detail level accepted by modern `getBlock` requests.
pub use self::rpc_blocks::SolanaTransactionDetails;
/// Optional typed configuration for the `getBalance` canary.
pub use self::rpc_canary::GetBalanceConfig;
/// Typed lamport balance returned by the `getBalance` canary.
@@ -172,6 +197,18 @@ pub use self::rpc_common::SolanaRpcResponse;
pub(crate) use self::rpc_common::decode_wire_json;
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
pub(crate) use self::rpc_common::parse_wire_pubkey;
/// Inflation-governor values returned by `getInflationGovernor`.
pub use self::rpc_economics::SolanaInflationGovernor;
/// Current inflation-rate values returned by `getInflationRate`.
pub use self::rpc_economics::SolanaInflationRate;
/// One non-null positional reward returned by `getInflationReward`.
pub use self::rpc_economics::SolanaInflationReward;
/// Optional epoch/context configuration accepted by `getInflationReward`.
pub use self::rpc_economics::SolanaInflationRewardConfig;
/// Supply totals returned inside the contextual `getSupply` response.
pub use self::rpc_economics::SolanaSupply;
/// Optional configuration accepted by `getSupply`.
pub use self::rpc_economics::SolanaSupplyConfig;
/// 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.

View File

@@ -0,0 +1,712 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
// version: 1
/// Transaction detail level accepted by modern `getBlock` requests.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum SolanaTransactionDetails {
/// Return complete transaction payloads and metadata.
#[default]
Full,
/// Return transaction signatures only.
Signatures,
/// Return no transaction payloads.
None,
/// Return the account-list transaction representation.
Accounts,
}
impl SolanaTransactionDetails {
/// Returns the Solana JSON-RPC wire string.
#[must_use]
pub const fn as_str(self) -> &'static str {
return match self {
Self::Full => "full",
Self::Signatures => "signatures",
Self::None => "none",
Self::Accounts => "accounts",
};
}
}
/// Modern configuration object accepted by `getBlock`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SolanaGetBlockConfig {
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionEncoding>,
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
max_supported_transaction_version: std::option::Option<u8>,
rewards: std::option::Option<bool>,
}
impl SolanaGetBlockConfig {
/// Creates a modern `getBlock` configuration.
#[must_use]
pub const fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
encoding: std::option::Option<crate::SolanaTransactionEncoding>,
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
max_supported_transaction_version: std::option::Option<u8>,
rewards: std::option::Option<bool>,
) -> Self {
return Self { commitment, encoding, transaction_details, max_supported_transaction_version, rewards };
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns the optional transaction encoding.
#[must_use]
pub const fn encoding(&self) -> std::option::Option<crate::SolanaTransactionEncoding> {
return self.encoding;
}
/// Returns the optional transaction detail level.
#[must_use]
pub const fn transaction_details(&self) -> std::option::Option<crate::SolanaTransactionDetails> {
return self.transaction_details;
}
/// Returns the highest transaction version the caller declares it can consume.
#[must_use]
pub const fn max_supported_transaction_version(&self) -> std::option::Option<u8> {
return self.max_supported_transaction_version;
}
/// Returns whether rewards were explicitly requested or suppressed.
#[must_use]
pub const fn rewards(&self) -> std::option::Option<bool> {
return self.rewards;
}
/// Returns whether this config would serialize to an empty object.
pub(crate) const fn is_empty(&self) -> bool {
return self.commitment.is_none()
&& self.encoding.is_none()
&& self.transaction_details.is_none()
&& self.max_supported_transaction_version.is_none()
&& self.rewards.is_none();
}
/// Serializes this config to the exact Solana JSON-RPC object.
#[must_use]
pub(crate) fn to_json_value(self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(commitment) = self.commitment {
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
}
if let std::option::Option::Some(encoding) = self.encoding {
object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned()));
}
if let std::option::Option::Some(details) = self.transaction_details {
object.insert("transactionDetails".to_owned(), serde_json::Value::String(details.as_str().to_owned()));
}
if let std::option::Option::Some(version) = self.max_supported_transaction_version {
object.insert("maxSupportedTransactionVersion".to_owned(), serde_json::Value::Number(version.into()));
}
if let std::option::Option::Some(rewards) = self.rewards {
object.insert("rewards".to_owned(), serde_json::Value::Bool(rewards));
}
return serde_json::Value::Object(object);
}
}
/// Slot range accepted inside `getBlockProduction` configuration.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SolanaBlockProductionRange {
first_slot: u64,
last_slot: std::option::Option<u64>,
}
impl SolanaBlockProductionRange {
/// Creates a block-production range.
#[must_use]
pub const fn new(first_slot: u64, last_slot: std::option::Option<u64>) -> Self {
return Self { first_slot, last_slot };
}
/// Returns the first slot in the requested range.
#[must_use]
pub const fn first_slot(&self) -> u64 {
return self.first_slot;
}
/// Returns the optional last slot in the requested range.
#[must_use]
pub const fn last_slot(&self) -> std::option::Option<u64> {
return self.last_slot;
}
/// Serializes this range to the exact Solana JSON-RPC object.
#[must_use]
pub(crate) fn to_json_value(self) -> serde_json::Value {
let mut object = serde_json::Map::new();
object.insert("firstSlot".to_owned(), serde_json::Value::Number(self.first_slot.into()));
if let std::option::Option::Some(last_slot) = self.last_slot {
object.insert("lastSlot".to_owned(), serde_json::Value::Number(last_slot.into()));
}
return serde_json::Value::Object(object);
}
}
/// Optional configuration accepted by `getBlockProduction`.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SolanaBlockProductionConfig {
commitment: std::option::Option<crate::SolanaCommitment>,
identity: std::option::Option<ksp_core_lib::Pubkey>,
range: std::option::Option<crate::SolanaBlockProductionRange>,
}
impl SolanaBlockProductionConfig {
/// Creates a block-production configuration.
#[must_use]
pub const fn new(
commitment: std::option::Option<crate::SolanaCommitment>,
identity: std::option::Option<ksp_core_lib::Pubkey>,
range: std::option::Option<crate::SolanaBlockProductionRange>,
) -> Self {
return Self { commitment, identity, range };
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.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 slot range.
#[must_use]
pub const fn range(&self) -> std::option::Option<&crate::SolanaBlockProductionRange> {
return self.range.as_ref();
}
/// Returns whether this config would serialize to an empty object.
pub(crate) const fn is_empty(&self) -> bool {
return self.commitment.is_none() && self.identity.is_none() && self.range.is_none();
}
/// Serializes this config to the exact Solana JSON-RPC 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(identity) = self.identity.as_ref() {
object.insert("identity".to_owned(), serde_json::Value::String(identity.to_string()));
}
if let std::option::Option::Some(range) = self.range {
object.insert("range".to_owned(), range.to_json_value());
}
return serde_json::Value::Object(object);
}
}
/// Commitment distribution returned by `getBlockCommitment`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolanaBlockCommitment {
commitment: std::option::Option<std::vec::Vec<u64>>,
total_stake: u64,
}
impl SolanaBlockCommitment {
/// Returns the nullable commitment stake distribution.
#[must_use]
pub fn commitment(&self) -> std::option::Option<&[u64]> {
return self.commitment.as_deref();
}
/// Returns the total active stake considered by the response.
#[must_use]
pub const fn total_stake(&self) -> u64 {
return self.total_stake;
}
/// Decodes a block-commitment result from its 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::<WireBlockCommitment>(method, value);
return match decoded {
std::result::Result::Ok(wire) => std::result::Result::Ok(Self { commitment: wire.commitment, total_stake: wire.total_stake }),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Effective range reported inside `getBlockProduction` results.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SolanaBlockProductionResultRange {
first_slot: u64,
last_slot: u64,
}
impl SolanaBlockProductionResultRange {
/// Returns the first slot in the effective range.
#[must_use]
pub const fn first_slot(&self) -> u64 {
return self.first_slot;
}
/// Returns the last slot in the effective range.
#[must_use]
pub const fn last_slot(&self) -> u64 {
return self.last_slot;
}
}
/// Block-production counts returned by `getBlockProduction` before the shared RPC context wrapper is applied.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolanaBlockProduction {
by_identity: std::collections::BTreeMap<ksp_core_lib::Pubkey, (usize, usize)>,
range: crate::SolanaBlockProductionResultRange,
}
impl SolanaBlockProduction {
/// Returns validator identities mapped to `(leader slots, blocks produced)` counts.
#[must_use]
pub const fn by_identity(&self) -> &std::collections::BTreeMap<ksp_core_lib::Pubkey, (usize, usize)> {
return &self.by_identity;
}
/// Returns the effective slot range reported by the runtime.
#[must_use]
pub const fn range(&self) -> &crate::SolanaBlockProductionResultRange {
return &self.range;
}
/// Decodes a block-production result from its 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::<WireBlockProduction>(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 by_identity = std::collections::BTreeMap::new();
for (identity, counts) in wire.by_identity {
let parsed = crate::parse_wire_pubkey(method, "byIdentity", identity.as_str());
let identity = match parsed {
std::result::Result::Ok(identity) => identity,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
by_identity.insert(identity, counts);
}
return std::result::Result::Ok(Self {
by_identity,
range: crate::SolanaBlockProductionResultRange { first_slot: wire.range.first_slot, last_slot: wire.range.last_slot },
});
}
}
/// One reward entry returned in a confirmed block.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaBlockReward {
pubkey: ksp_core_lib::Pubkey,
lamports: i64,
post_balance: u64,
reward_type: std::option::Option<std::string::String>,
commission: std::option::Option<u8>,
commission_bps: crate::SolanaWireField<u16>,
}
impl SolanaBlockReward {
/// Returns the rewarded account public key.
#[must_use]
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
/// Returns the signed lamport delta applied by the reward.
#[must_use]
pub const fn lamports(&self) -> i64 {
return self.lamports;
}
/// Returns the post-reward account balance in lamports.
#[must_use]
pub const fn post_balance(&self) -> u64 {
return self.post_balance;
}
/// Returns the runtime reward type text without imposing a local economic taxonomy.
#[must_use]
pub fn reward_type(&self) -> std::option::Option<&str> {
return self.reward_type.as_deref();
}
/// Returns the nullable legacy/effective commission percentage.
#[must_use]
pub const fn commission(&self) -> std::option::Option<u8> {
return self.commission;
}
/// Returns the basis-point commission while preserving omitted/null/present wire states.
#[must_use]
pub const fn commission_bps(&self) -> &crate::SolanaWireField<u16> {
return &self.commission_bps;
}
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
let decoded = crate::decode_wire_json::<WireBlockReward>(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, "rewards.pubkey", wire.pubkey.as_str());
return match pubkey {
std::result::Result::Ok(pubkey) => std::result::Result::Ok(Self {
pubkey,
lamports: wire.lamports,
post_balance: wire.post_balance,
reward_type: wire.reward_type,
commission: wire.commission,
commission_bps: wire.commission_bps,
}),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// One transaction entry returned inside a confirmed block.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaBlockTransaction {
transaction: crate::SolanaEncodedTransaction,
meta: crate::SolanaWireField<serde_json::Value>,
version: crate::SolanaWireField<crate::SolanaTransactionVersion>,
}
impl SolanaBlockTransaction {
/// Returns the encoded or JSON transaction payload without Program-specific decoding.
#[must_use]
pub const fn transaction(&self) -> &crate::SolanaEncodedTransaction {
return &self.transaction;
}
/// Returns transaction metadata while preserving omission versus explicit `null`.
#[must_use]
pub const fn meta(&self) -> &crate::SolanaWireField<serde_json::Value> {
return &self.meta;
}
/// Returns the transaction version while preserving omission versus explicit `null`.
#[must_use]
pub const fn version(&self) -> &crate::SolanaWireField<crate::SolanaTransactionVersion> {
return &self.version;
}
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
let decoded = crate::decode_wire_json::<WireBlockTransaction>(method, value);
let wire = match decoded {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transaction = crate::SolanaEncodedTransaction::decode_wire(method, wire.transaction);
let transaction = match transaction {
std::result::Result::Ok(transaction) => transaction,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let version = decode_block_transaction_version(method, wire.version);
return match version {
std::result::Result::Ok(version) => std::result::Result::Ok(Self { transaction, meta: wire.meta, version }),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Confirmed block wire result returned by `getBlock` when the RPC result is non-null.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaConfirmedBlock {
previous_blockhash: std::string::String,
blockhash: std::string::String,
parent_slot: u64,
transactions: crate::SolanaWireField<std::vec::Vec<crate::SolanaBlockTransaction>>,
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
rewards: crate::SolanaWireField<std::vec::Vec<crate::SolanaBlockReward>>,
num_reward_partitions: crate::SolanaWireField<u64>,
block_time: std::option::Option<i64>,
block_height: std::option::Option<u64>,
}
impl SolanaConfirmedBlock {
/// Returns the previous blockhash exactly as reported by the RPC wire.
#[must_use]
pub fn previous_blockhash(&self) -> &str {
return self.previous_blockhash.as_str();
}
/// Returns this block's blockhash exactly as reported by the RPC wire.
#[must_use]
pub fn blockhash(&self) -> &str {
return self.blockhash.as_str();
}
/// Returns the parent slot.
#[must_use]
pub const fn parent_slot(&self) -> u64 {
return self.parent_slot;
}
/// Returns block transactions while preserving omitted/null/present states.
#[must_use]
pub const fn transactions(&self) -> &crate::SolanaWireField<std::vec::Vec<crate::SolanaBlockTransaction>> {
return &self.transactions;
}
/// Returns block signatures while preserving omitted/null/present states.
#[must_use]
pub const fn signatures(&self) -> &crate::SolanaWireField<std::vec::Vec<std::string::String>> {
return &self.signatures;
}
/// Returns block rewards while preserving omitted/null/present states.
#[must_use]
pub const fn rewards(&self) -> &crate::SolanaWireField<std::vec::Vec<crate::SolanaBlockReward>> {
return &self.rewards;
}
/// Returns the SIMD-0118 reward-partition count while preserving omission versus explicit `null`.
#[must_use]
pub const fn num_reward_partitions(&self) -> &crate::SolanaWireField<u64> {
return &self.num_reward_partitions;
}
/// Returns the nullable block time as a Unix timestamp.
#[must_use]
pub const fn block_time(&self) -> std::option::Option<i64> {
return self.block_time;
}
/// Returns the nullable block height.
#[must_use]
pub const fn block_height(&self) -> std::option::Option<u64> {
return self.block_height;
}
/// Decodes a confirmed block from its 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::<WireConfirmedBlock>(method, value);
let wire = match decoded {
std::result::Result::Ok(wire) => wire,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transactions = decode_block_transactions(method, wire.transactions);
let transactions = match transactions {
std::result::Result::Ok(transactions) => transactions,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rewards = decode_block_rewards(method, wire.rewards);
let rewards = match rewards {
std::result::Result::Ok(rewards) => rewards,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self {
previous_blockhash: wire.previous_blockhash,
blockhash: wire.blockhash,
parent_slot: wire.parent_slot,
transactions,
signatures: wire.signatures,
rewards,
num_reward_partitions: wire.num_reward_partitions,
block_time: wire.block_time,
block_height: wire.block_height,
});
}
}
/// One entry returned by `getRecentPerformanceSamples`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SolanaPerformanceSample {
slot: u64,
num_transactions: u64,
num_non_vote_transactions: std::option::Option<u64>,
num_slots: u64,
sample_period_secs: u16,
}
impl SolanaPerformanceSample {
/// Returns the sample's highest slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the total transaction count in the sample.
#[must_use]
pub const fn num_transactions(&self) -> u64 {
return self.num_transactions;
}
/// Returns the optional non-vote transaction count exposed by newer runtimes.
#[must_use]
pub const fn num_non_vote_transactions(&self) -> std::option::Option<u64> {
return self.num_non_vote_transactions;
}
/// Returns the number of slots covered by the sample.
#[must_use]
pub const fn num_slots(&self) -> u64 {
return self.num_slots;
}
/// Returns the sample duration in seconds.
#[must_use]
pub const fn sample_period_secs(&self) -> u16 {
return self.sample_period_secs;
}
/// Decodes one performance sample from its 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::<WirePerformanceSample>(method, value);
return match decoded {
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
slot: wire.slot,
num_transactions: wire.num_transactions,
num_non_vote_transactions: wire.num_non_vote_transactions,
num_slots: wire.num_slots,
sample_period_secs: wire.sample_period_secs,
}),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
fn decode_block_transaction_version(
method: &str,
field: crate::SolanaWireField<serde_json::Value>,
) -> ksp_core_lib::Result<crate::SolanaWireField<crate::SolanaTransactionVersion>> {
return match field {
crate::SolanaWireField::Omitted => std::result::Result::Ok(crate::SolanaWireField::Omitted),
crate::SolanaWireField::Null => std::result::Result::Ok(crate::SolanaWireField::Null),
crate::SolanaWireField::Value(value) => {
let decoded = crate::SolanaTransactionVersion::decode_wire(method, value);
match decoded {
std::result::Result::Ok(version) => std::result::Result::Ok(crate::SolanaWireField::Value(version)),
std::result::Result::Err(error) => std::result::Result::Err(error),
}
},
};
}
fn decode_block_transactions(
method: &str,
field: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
) -> ksp_core_lib::Result<crate::SolanaWireField<std::vec::Vec<crate::SolanaBlockTransaction>>> {
let values = match field {
crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted),
crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null),
crate::SolanaWireField::Value(values) => values,
};
let mut transactions = std::vec::Vec::with_capacity(values.len());
for value in values {
let decoded = crate::SolanaBlockTransaction::decode_wire(method, value);
match decoded {
std::result::Result::Ok(transaction) => transactions.push(transaction),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(crate::SolanaWireField::Value(transactions));
}
fn decode_block_rewards(
method: &str,
field: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
) -> ksp_core_lib::Result<crate::SolanaWireField<std::vec::Vec<crate::SolanaBlockReward>>> {
let values = match field {
crate::SolanaWireField::Omitted => return std::result::Result::Ok(crate::SolanaWireField::Omitted),
crate::SolanaWireField::Null => return std::result::Result::Ok(crate::SolanaWireField::Null),
crate::SolanaWireField::Value(values) => values,
};
let mut rewards = std::vec::Vec::with_capacity(values.len());
for value in values {
let decoded = crate::SolanaBlockReward::decode_wire(method, value);
match decoded {
std::result::Result::Ok(reward) => rewards.push(reward),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(crate::SolanaWireField::Value(rewards));
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockCommitment {
commitment: std::option::Option<std::vec::Vec<u64>>,
total_stake: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProductionRange {
first_slot: u64,
last_slot: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProduction {
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
range: WireBlockProductionRange,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockReward {
pubkey: std::string::String,
lamports: i64,
post_balance: u64,
#[serde(default)]
reward_type: std::option::Option<std::string::String>,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockTransaction {
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedBlock {
previous_blockhash: std::string::String,
blockhash: std::string::String,
parent_slot: u64,
#[serde(default)]
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
num_reward_partitions: crate::SolanaWireField<u64>,
block_time: std::option::Option<i64>,
block_height: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePerformanceSample {
slot: u64,
num_transactions: u64,
#[serde(default)]
num_non_vote_transactions: std::option::Option<u64>,
num_slots: u64,
sample_period_secs: u16,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_blocks.rs"]
mod tests;

View File

@@ -0,0 +1,377 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs
// version: 1
/// Inflation-governor values returned by `getInflationGovernor`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SolanaInflationGovernor {
initial: f64,
terminal: f64,
taper: f64,
foundation: f64,
foundation_term: f64,
}
impl SolanaInflationGovernor {
/// Returns the initial inflation rate.
#[must_use]
pub const fn initial(&self) -> f64 {
return self.initial;
}
/// Returns the terminal inflation rate.
#[must_use]
pub const fn terminal(&self) -> f64 {
return self.terminal;
}
/// Returns the taper/disinflation parameter reported by the runtime.
#[must_use]
pub const fn taper(&self) -> f64 {
return self.taper;
}
/// Returns the foundation allocation rate.
#[must_use]
pub const fn foundation(&self) -> f64 {
return self.foundation;
}
/// Returns the foundation allocation term.
#[must_use]
pub const fn foundation_term(&self) -> f64 {
return self.foundation_term;
}
/// Decodes an inflation-governor result from its 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::<WireInflationGovernor>(method, value);
return match decoded {
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
initial: wire.initial,
terminal: wire.terminal,
taper: wire.taper,
foundation: wire.foundation,
foundation_term: wire.foundation_term,
}),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Current inflation-rate values returned by `getInflationRate`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SolanaInflationRate {
total: f64,
validator: f64,
foundation: f64,
epoch: u64,
}
impl SolanaInflationRate {
/// Returns the total inflation rate.
#[must_use]
pub const fn total(&self) -> f64 {
return self.total;
}
/// Returns the validator inflation rate.
#[must_use]
pub const fn validator(&self) -> f64 {
return self.validator;
}
/// Returns the foundation inflation rate.
#[must_use]
pub const fn foundation(&self) -> f64 {
return self.foundation;
}
/// Returns the epoch associated with the rate.
#[must_use]
pub const fn epoch(&self) -> u64 {
return self.epoch;
}
/// Decodes an inflation-rate result from its 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::<WireInflationRate>(method, value);
return match decoded {
std::result::Result::Ok(wire) => {
std::result::Result::Ok(Self { total: wire.total, validator: wire.validator, foundation: wire.foundation, epoch: wire.epoch })
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Optional epoch/context configuration accepted by `getInflationReward`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SolanaInflationRewardConfig {
epoch: std::option::Option<u64>,
commitment: std::option::Option<crate::SolanaCommitment>,
min_context_slot: std::option::Option<u64>,
}
impl SolanaInflationRewardConfig {
/// Creates an inflation-reward configuration.
#[must_use]
pub const fn new(
epoch: std::option::Option<u64>,
commitment: std::option::Option<crate::SolanaCommitment>,
min_context_slot: std::option::Option<u64>,
) -> Self {
return Self { epoch, commitment, min_context_slot };
}
/// Returns the optional target epoch.
#[must_use]
pub const fn epoch(&self) -> std::option::Option<u64> {
return self.epoch;
}
/// 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 would serialize to an empty object.
pub(crate) const fn is_empty(&self) -> bool {
return self.epoch.is_none() && self.commitment.is_none() && self.min_context_slot.is_none();
}
/// Serializes this config to the exact Solana JSON-RPC 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(epoch) = self.epoch {
object.insert("epoch".to_owned(), serde_json::Value::Number(epoch.into()));
}
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);
}
}
/// One non-null positional reward returned by `getInflationReward`.
#[derive(Clone, Debug, PartialEq)]
pub struct SolanaInflationReward {
epoch: u64,
effective_slot: u64,
amount: u64,
post_balance: u64,
commission: std::option::Option<u8>,
commission_bps: crate::SolanaWireField<u16>,
}
impl SolanaInflationReward {
/// Returns the rewarded epoch.
#[must_use]
pub const fn epoch(&self) -> u64 {
return self.epoch;
}
/// Returns the slot at which the reward became effective.
#[must_use]
pub const fn effective_slot(&self) -> u64 {
return self.effective_slot;
}
/// Returns the reward amount in lamports.
#[must_use]
pub const fn amount(&self) -> u64 {
return self.amount;
}
/// Returns the post-reward account balance in lamports.
#[must_use]
pub const fn post_balance(&self) -> u64 {
return self.post_balance;
}
/// Returns the nullable legacy/effective commission percentage.
#[must_use]
pub const fn commission(&self) -> std::option::Option<u8> {
return self.commission;
}
/// Returns the basis-point commission while preserving omitted/null/present wire states.
#[must_use]
pub const fn commission_bps(&self) -> &crate::SolanaWireField<u16> {
return &self.commission_bps;
}
/// Decodes one non-null inflation reward from its 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::<WireInflationReward>(method, value);
return match decoded {
std::result::Result::Ok(wire) => std::result::Result::Ok(Self {
epoch: wire.epoch,
effective_slot: wire.effective_slot,
amount: wire.amount,
post_balance: wire.post_balance,
commission: wire.commission,
commission_bps: wire.commission_bps,
}),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
/// Optional configuration accepted by `getSupply`.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SolanaSupplyConfig {
commitment: std::option::Option<crate::SolanaCommitment>,
exclude_non_circulating_accounts_list: std::option::Option<bool>,
}
impl SolanaSupplyConfig {
/// Creates a supply configuration.
#[must_use]
pub const fn new(commitment: std::option::Option<crate::SolanaCommitment>, exclude_non_circulating_accounts_list: std::option::Option<bool>) -> Self {
return Self { commitment, exclude_non_circulating_accounts_list };
}
/// Returns the optional commitment level.
#[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns whether the request explicitly controls omission of the non-circulating account list.
#[must_use]
pub const fn exclude_non_circulating_accounts_list(&self) -> std::option::Option<bool> {
return self.exclude_non_circulating_accounts_list;
}
/// Returns whether this config would serialize to an empty object.
pub(crate) const fn is_empty(&self) -> bool {
return self.commitment.is_none() && self.exclude_non_circulating_accounts_list.is_none();
}
/// Serializes this config to the exact Solana JSON-RPC 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(exclude) = self.exclude_non_circulating_accounts_list {
object.insert("excludeNonCirculatingAccountsList".to_owned(), serde_json::Value::Bool(exclude));
}
return serde_json::Value::Object(object);
}
}
/// Supply totals returned inside the contextual `getSupply` response.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SolanaSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<ksp_core_lib::Pubkey>,
}
impl SolanaSupply {
/// Returns the total supply in lamports.
#[must_use]
pub const fn total(&self) -> u64 {
return self.total;
}
/// Returns the circulating supply in lamports.
#[must_use]
pub const fn circulating(&self) -> u64 {
return self.circulating;
}
/// Returns the non-circulating supply in lamports.
#[must_use]
pub const fn non_circulating(&self) -> u64 {
return self.non_circulating;
}
/// Returns the ordered non-circulating account list supplied by the runtime.
#[must_use]
pub fn non_circulating_accounts(&self) -> &[ksp_core_lib::Pubkey] {
return self.non_circulating_accounts.as_slice();
}
/// Decodes a supply result from its 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::<WireSupply>(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 accounts = std::vec::Vec::with_capacity(wire.non_circulating_accounts.len());
for account in wire.non_circulating_accounts {
let parsed = crate::parse_wire_pubkey(method, "nonCirculatingAccounts", account.as_str());
match parsed {
std::result::Result::Ok(pubkey) => accounts.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(Self {
total: wire.total,
circulating: wire.circulating,
non_circulating: wire.non_circulating,
non_circulating_accounts: accounts,
});
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationGovernor {
initial: f64,
terminal: f64,
taper: f64,
foundation: f64,
foundation_term: f64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationRate {
total: f64,
validator: f64,
foundation: f64,
epoch: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationReward {
epoch: u64,
effective_slot: u64,
amount: u64,
post_balance: u64,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<std::string::String>,
}
#[cfg(test)]
#[path = "../unit_tests/rpc_economics.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 7
// version: 8
/// Binary encoding accepted for serialized transaction input payloads.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -838,7 +838,8 @@ pub enum SolanaTransactionVersion {
}
impl SolanaTransactionVersion {
fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
/// Decodes a transaction version from the shared Solana JSON wire representation.
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
return match value {
serde_json::Value::String(value) if value == "legacy" => std::result::Result::Ok(Self::Legacy),
serde_json::Value::Number(value) => {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 15
// version: 16
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -347,3 +347,42 @@ fn public_transaction_pre_007_simulation_wrapper_and_complete_config_are_availab
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
);
}
#[test]
fn public_v0_2_4_pre_002_blocks_economics_types_are_available_from_crate_root() {
let block = ksp_onchain_transport_lib::SolanaGetBlockConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Json),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
std::option::Option::Some(1),
std::option::Option::Some(true),
);
assert_eq!(block.max_supported_transaction_version(), std::option::Option::Some(1));
let range = ksp_onchain_transport_lib::SolanaBlockProductionRange::new(100, std::option::Option::Some(200));
assert_eq!(range.last_slot(), std::option::Option::Some(200));
let production_config = ksp_onchain_transport_lib::SolanaBlockProductionConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
std::option::Option::None,
std::option::Option::Some(range),
);
assert_eq!(production_config.range().map(ksp_onchain_transport_lib::SolanaBlockProductionRange::first_slot), std::option::Option::Some(100));
let inflation = ksp_onchain_transport_lib::SolanaInflationRewardConfig::new(
std::option::Option::Some(812),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
std::option::Option::Some(430_000_000),
);
assert_eq!(inflation.epoch(), std::option::Option::Some(812));
let supply = ksp_onchain_transport_lib::SolanaSupplyConfig::new(std::option::Option::None, std::option::Option::Some(false));
assert_eq!(supply.exclude_non_circulating_accounts_list(), std::option::Option::Some(false));
let _block_commitment: std::option::Option<ksp_onchain_transport_lib::SolanaBlockCommitment> = std::option::Option::None;
let _block_production: std::option::Option<ksp_onchain_transport_lib::SolanaBlockProduction> = std::option::Option::None;
let _block_production_result_range: std::option::Option<ksp_onchain_transport_lib::SolanaBlockProductionResultRange> = std::option::Option::None;
let _block_reward: std::option::Option<ksp_onchain_transport_lib::SolanaBlockReward> = std::option::Option::None;
let _block_transaction: std::option::Option<ksp_onchain_transport_lib::SolanaBlockTransaction> = std::option::Option::None;
let _confirmed_block: std::option::Option<ksp_onchain_transport_lib::SolanaConfirmedBlock> = std::option::Option::None;
let _performance_sample: std::option::Option<ksp_onchain_transport_lib::SolanaPerformanceSample> = std::option::Option::None;
let _governor: std::option::Option<ksp_onchain_transport_lib::SolanaInflationGovernor> = std::option::Option::None;
let _rate: std::option::Option<ksp_onchain_transport_lib::SolanaInflationRate> = std::option::Option::None;
let _reward: std::option::Option<ksp_onchain_transport_lib::SolanaInflationReward> = std::option::Option::None;
let _supply: std::option::Option<ksp_onchain_transport_lib::SolanaSupply> = std::option::Option::None;
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 13
// version: 14
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
//! Release-level completeness canaries for the staged HTTP wrapper sequence.
#[test]
fn release_registry_partition_matches_the_audited_http_plan() {
@@ -430,3 +430,44 @@ fn release_pre_008_ksp_transport_007_retro_audit_covers_all_typed_current_method
let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
assert!(get_transaction.request_form_status().has_deprecated_legacy());
}
#[test]
fn release_v0_2_4_descriptor_set_is_exact_and_remains_read_retry_safe_before_wrappers() {
let mut expected_blocks = std::vec![
"getBlock",
"getBlockCommitment",
"getBlockHeight",
"getBlockProduction",
"getBlocks",
"getBlocksWithLimit",
"getBlockTime",
"getFirstAvailableBlock",
"getRecentPerformanceSamples",
"minimumLedgerSlot",
];
let mut expected_economics = std::vec!["getInflationGovernor", "getInflationRate", "getInflationReward", "getStakeMinimumDelegation", "getSupply",];
let mut actual_blocks = std::vec::Vec::new();
let mut actual_economics = std::vec::Vec::new();
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_4 {
continue;
}
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
match descriptor.category() {
ksp_onchain_transport_lib::HttpRpcCategory::Blocks => actual_blocks.push(descriptor.method()),
ksp_onchain_transport_lib::HttpRpcCategory::Economics => actual_economics.push(descriptor.method()),
_ => {},
}
}
actual_blocks.sort_unstable();
actual_economics.sort_unstable();
expected_blocks.sort_unstable();
expected_economics.sort_unstable();
assert_eq!(actual_blocks, expected_blocks);
assert_eq!(actual_economics, expected_economics);
assert_eq!(actual_blocks.len(), 10);
assert_eq!(actual_economics.len(), 5);
let get_block = ksp_onchain_transport_lib::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
assert!(get_block.request_form_status().has_deprecated_legacy());
}

View File

@@ -0,0 +1,150 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
// version: 1
#[test]
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
assert_eq!(crate::SolanaTransactionDetails::Full.as_str(), "full");
assert_eq!(crate::SolanaTransactionDetails::Signatures.as_str(), "signatures");
assert_eq!(crate::SolanaTransactionDetails::None.as_str(), "none");
assert_eq!(crate::SolanaTransactionDetails::Accounts.as_str(), "accounts");
let config = crate::SolanaGetBlockConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
std::option::Option::Some(crate::SolanaTransactionDetails::Accounts),
std::option::Option::Some(1),
std::option::Option::Some(false),
);
assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed));
assert_eq!(config.encoding(), std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed));
assert_eq!(config.transaction_details(), std::option::Option::Some(crate::SolanaTransactionDetails::Accounts));
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(1));
assert_eq!(config.rewards(), std::option::Option::Some(false));
assert_eq!(
config.to_json_value(),
serde_json::json!({
"commitment":"confirmed",
"encoding":"jsonParsed",
"transactionDetails":"accounts",
"maxSupportedTransactionVersion":1,
"rewards":false
})
);
assert!(crate::SolanaGetBlockConfig::default().is_empty());
assert_eq!(crate::SolanaGetBlockConfig::default().to_json_value(), serde_json::json!({}));
}
#[test]
fn block_production_config_preserves_identity_range_and_commitment() {
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture identity must parse");
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::Some(430_000_099));
let config = crate::SolanaBlockProductionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Finalized),
std::option::Option::Some(identity),
std::option::Option::Some(range),
);
assert_eq!(config.identity(), std::option::Option::Some(&identity));
assert_eq!(config.range().expect("range must be present").first_slot(), 430_000_000);
assert_eq!(config.range().expect("range must be present").last_slot(), std::option::Option::Some(430_000_099));
assert_eq!(
config.to_json_value(),
serde_json::json!({
"commitment":"finalized",
"identity":"11111111111111111111111111111111",
"range":{"firstSlot":430000000,"lastSlot":430000099}
})
);
assert!(crate::SolanaBlockProductionConfig::default().is_empty());
}
#[test]
fn block_commitment_fixture_preserves_nullable_distribution() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/block_commitment.variants.json"))
.expect("block commitment fixtures must decode");
let present = crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", values[0].clone()).expect("present commitment must decode");
let absent = crate::SolanaBlockCommitment::decode_wire("getBlockCommitment", values[1].clone()).expect("null commitment must decode");
assert_eq!(present.commitment(), std::option::Option::Some(&[1, 2, 3, 4][..]));
assert_eq!(present.total_stake(), 1_000_000_000);
assert_eq!(absent.commitment(), std::option::Option::None);
}
#[test]
fn block_production_fixture_types_identity_keys_and_preserves_counts() {
let value =
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/block_production.v4_2_1.json")).expect("block production fixture must decode");
let result = crate::SolanaBlockProduction::decode_wire("getBlockProduction", value).expect("block production result must decode");
let identity = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture identity must parse");
assert_eq!(result.by_identity().get(&identity), std::option::Option::Some(&(8, 7)));
assert_eq!(result.range().first_slot(), 430_000_000);
assert_eq!(result.range().last_slot(), 430_000_099);
}
#[test]
fn confirmed_block_fixture_preserves_rich_wire_and_simd_extensions() {
let value =
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/confirmed_block.v4_2_1.json")).expect("confirmed block fixture must decode");
let block = crate::SolanaConfirmedBlock::decode_wire("getBlock", value).expect("confirmed block must decode");
assert_eq!(block.parent_slot(), 430_000_122);
assert_eq!(block.block_time(), std::option::Option::Some(1_787_072_400));
assert_eq!(block.block_height(), std::option::Option::Some(410_000_000));
assert_eq!(block.num_reward_partitions().value(), std::option::Option::Some(&4));
let transactions = block.transactions().value().expect("transactions must be present");
assert_eq!(transactions.len(), 2);
assert!(matches!(transactions[0].version(), crate::SolanaWireField::Value(crate::SolanaTransactionVersion::Number(1))));
assert!(transactions[1].version().is_omitted());
assert!(transactions[1].meta().is_null());
let rewards = block.rewards().value().expect("rewards must be present");
assert_eq!(rewards.len(), 2);
assert_eq!(rewards[0].reward_type(), std::option::Option::Some("Fee"));
assert_eq!(rewards[0].commission(), std::option::Option::None);
assert_eq!(rewards[0].commission_bps().value(), std::option::Option::Some(&1_234));
assert_eq!(rewards[1].commission(), std::option::Option::Some(5));
assert!(rewards[1].commission_bps().is_omitted());
}
#[test]
fn confirmed_block_fixture_distinguishes_omitted_and_explicit_null_fields() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/confirmed_block.omissions.json"))
.expect("confirmed block omission fixtures must decode");
let omitted = crate::SolanaConfirmedBlock::decode_wire("getBlock", values[0].clone()).expect("omitted-fields block must decode");
let nulls = crate::SolanaConfirmedBlock::decode_wire("getBlock", values[1].clone()).expect("null-fields block must decode");
assert!(omitted.transactions().is_omitted());
assert!(omitted.signatures().is_omitted());
assert!(omitted.rewards().is_omitted());
assert!(omitted.num_reward_partitions().is_omitted());
assert!(nulls.transactions().is_null());
assert!(nulls.signatures().is_null());
assert!(nulls.rewards().is_null());
assert!(nulls.num_reward_partitions().is_null());
}
#[test]
fn performance_sample_fixture_accepts_current_and_older_shapes() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/performance_sample.variants.json"))
.expect("performance sample fixtures must decode");
let current = crate::SolanaPerformanceSample::decode_wire("getRecentPerformanceSamples", values[0].clone()).expect("current sample must decode");
let older = crate::SolanaPerformanceSample::decode_wire("getRecentPerformanceSamples", values[1].clone()).expect("older sample must decode");
assert_eq!(current.slot(), 430_000_123);
assert_eq!(current.num_transactions(), 250_000);
assert_eq!(current.num_non_vote_transactions(), std::option::Option::Some(175_000));
assert_eq!(current.num_slots(), 120);
assert_eq!(current.sample_period_secs(), 60);
assert_eq!(older.num_non_vote_transactions(), std::option::Option::None);
}
#[test]
fn block_reward_rejects_invalid_pubkey_without_echoing_value() {
let result = crate::SolanaConfirmedBlock::decode_wire(
"getBlock",
serde_json::json!({
"previousBlockhash":"previous",
"blockhash":"block",
"parentSlot":1,
"rewards":[{"pubkey":"not-a-pubkey","lamports":1,"postBalance":2,"rewardType":"Fee","commission":null}],
"blockTime":null,
"blockHeight":null
}),
);
let error = result.expect_err("invalid reward pubkey must fail closed");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
assert!(!error.to_string().contains("not-a-pubkey"));
}

View File

@@ -0,0 +1,86 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
// version: 1
#[test]
fn inflation_reward_config_preserves_epoch_commitment_and_min_context_slot() {
let config = crate::SolanaInflationRewardConfig::new(
std::option::Option::Some(811),
std::option::Option::Some(crate::SolanaCommitment::Finalized),
std::option::Option::Some(429_000_000),
);
assert_eq!(config.epoch(), std::option::Option::Some(811));
assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized));
assert_eq!(config.min_context_slot(), std::option::Option::Some(429_000_000));
assert_eq!(config.to_json_value(), serde_json::json!({"epoch":811,"commitment":"finalized","minContextSlot":429000000}));
assert!(crate::SolanaInflationRewardConfig::default().is_empty());
}
#[test]
fn supply_config_distinguishes_omitted_from_explicit_false() {
let omitted = crate::SolanaSupplyConfig::default();
let explicit_false = crate::SolanaSupplyConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(false));
assert!(omitted.is_empty());
assert_eq!(omitted.to_json_value(), serde_json::json!({}));
assert_eq!(explicit_false.exclude_non_circulating_accounts_list(), std::option::Option::Some(false));
assert_eq!(explicit_false.to_json_value(), serde_json::json!({"commitment":"confirmed","excludeNonCirculatingAccountsList":false}));
}
#[test]
fn inflation_governor_and_rate_fixtures_preserve_runtime_values() {
let governor_value = serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/inflation_governor.v4_2_1.json"))
.expect("inflation governor fixture must decode");
let rate_value =
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/inflation_rate.v4_2_1.json")).expect("inflation rate fixture must decode");
let governor = crate::SolanaInflationGovernor::decode_wire("getInflationGovernor", governor_value).expect("inflation governor must decode");
let rate = crate::SolanaInflationRate::decode_wire("getInflationRate", rate_value).expect("inflation rate must decode");
assert_eq!(governor.initial(), 0.08);
assert_eq!(governor.terminal(), 0.015);
assert_eq!(governor.taper(), 0.15);
assert_eq!(governor.foundation(), 0.05);
assert_eq!(governor.foundation_term(), 7.0);
assert_eq!(rate.total(), 0.042);
assert_eq!(rate.validator(), 0.041);
assert_eq!(rate.foundation(), 0.001);
assert_eq!(rate.epoch(), 812);
}
#[test]
fn inflation_reward_fixture_preserves_position_null_and_commission_bps_states() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/inflation_reward.variants.json"))
.expect("inflation reward fixtures must decode");
let first = crate::SolanaInflationReward::decode_wire("getInflationReward", values[0].clone()).expect("first reward must decode");
let third = crate::SolanaInflationReward::decode_wire("getInflationReward", values[2].clone()).expect("third reward must decode");
assert_eq!(values.len(), 3);
assert!(values[1].is_null());
assert_eq!(first.epoch(), 811);
assert_eq!(first.effective_slot(), 429_900_000);
assert_eq!(first.amount(), 2_500_000);
assert_eq!(first.post_balance(), 1_002_500_000);
assert_eq!(first.commission(), std::option::Option::Some(7));
assert_eq!(first.commission_bps().value(), std::option::Option::Some(&725));
assert_eq!(third.commission(), std::option::Option::None);
assert!(third.commission_bps().is_omitted());
}
#[test]
fn supply_fixture_types_non_circulating_accounts_and_preserves_order() {
let value = serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/supply.v4_2_1.json")).expect("supply fixture must decode");
let supply = crate::SolanaSupply::decode_wire("getSupply", value).expect("supply must decode");
assert_eq!(supply.total(), 600_000_000_000_000_000);
assert_eq!(supply.circulating(), 500_000_000_000_000_000);
assert_eq!(supply.non_circulating(), 100_000_000_000_000_000);
assert_eq!(supply.non_circulating_accounts().len(), 2);
assert_eq!(supply.non_circulating_accounts()[0].to_string(), "11111111111111111111111111111111");
assert_eq!(supply.non_circulating_accounts()[1].to_string(), "Vote111111111111111111111111111111111111111");
}
#[test]
fn supply_rejects_invalid_non_circulating_pubkey_without_echoing_value() {
let result = crate::SolanaSupply::decode_wire(
"getSupply",
serde_json::json!({"total":3,"circulating":2,"nonCirculating":1,"nonCirculatingAccounts":["invalid-pubkey"]}),
);
let error = result.expect_err("invalid supply pubkey must fail closed");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
assert!(!error.to_string().contains("invalid-pubkey"));
}

331
deltas/0.2.4/pre.002.md Normal file
View File

@@ -0,0 +1,331 @@
<!-- file: deltas/0.2.4/pre.002.md -->
<!-- version: 1 -->
# Delta `0.2.4-pre.002` — primitives Blocks/Economics et fixtures wire partagées
## Base requise
Livraison précédente :
```text
0.2.4-pre.001-fix.001
workspace.package.version = "0.2.4-pre.1"
```
Le plan canonique est `docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md` version 2. Le fix `pre.001-fix.001` est appliqué avant cette tranche ; les deltas historiques `pre.001.md` et `pre.001-fix.001.md` restent inchangés.
## Objectif
Installer les primitives, configurations et résultats partagés nécessaires aux 10 wrappers Blocks et 5 wrappers Economics de `0.2.4`, ainsi que leurs fixtures wire locales déterministes, sans encore implémenter les wrappers prévus à partir de `pre.003` et sans déclarer la couverture typed `V0_2_4` complète.
Cette tranche matérialise dès maintenant les exigences `KSP-TRANSPORT-007` qui appartiennent au wire partagé : omissions/null explicites, versions de transaction numériques génériques, reward partitions SIMD-0118, commissions basis-points SIMD-0291, adresses RPC typées en `Pubkey` et sous-arbres Transaction riches conservés losslessly.
## Version Cargo
Conformément à `VER-ID-009`, la nouvelle prerelease synchronise le signal technique :
```text
0.2.4-pre.1 -> 0.2.4-pre.2
```
Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée.
## Implémentation
### Module Blocks partagé
Le nouveau module privé `rpc_blocks` ajoute les types publics suivants, réexportés explicitement depuis la racine de crate :
```text
SolanaTransactionDetails
SolanaGetBlockConfig
SolanaBlockProductionRange
SolanaBlockProductionConfig
SolanaBlockCommitment
SolanaBlockProductionResultRange
SolanaBlockProduction
SolanaBlockReward
SolanaBlockTransaction
SolanaConfirmedBlock
SolanaPerformanceSample
```
`SolanaTransactionDetails` couvre exactement les quatre valeurs wire modernes :
```text
full
signatures
none
accounts
```
`SolanaGetBlockConfig` conserve indépendamment les cinq options du contrat moderne :
```text
commitment
encoding
transactionDetails
maxSupportedTransactionVersion
rewards
```
Aucune validation de commitment ni aucun choix de forme legacy n'est encore exécuté ici : ces responsabilités appartiennent au wrapper `getBlock` de `pre.006`.
`SolanaBlockProductionConfig` conserve `commitment`, une `identity` typée `Pubkey` et une range `firstSlot/lastSlot?`. La validation `lastSlot >= firstSlot` reste volontairement réservée au wrapper `getBlockProduction` de `pre.005`.
`SolanaBlockProduction` décode les clés dynamiques de `byIdentity` en `Pubkey` et conserve les couples `(leader slots, blocks produced)` ainsi que la range effective retournée par le runtime. Le futur wrapper appliquera le `SolanaRpcResponse<T>` partagé autour de ce résultat.
### Wire `getBlock`
`SolanaConfirmedBlock` est un DTO bloc dédié et ne réutilise pas `SolanaConfirmedTransaction`, dont `slot` et `blockTime` sont propres au top-level de `getTransaction`.
Le bloc conserve :
```text
previousBlockhash
blockhash
parentSlot
transactions
signatures
rewards
numRewardPartitions
blockTime
blockHeight
```
Les champs conditionnels `transactions`, `signatures`, `rewards` et `numRewardPartitions` utilisent `SolanaWireField<T>` afin de préserver les trois états :
```text
Omitted
Null
Value(T)
```
`numRewardPartitions` couvre explicitement le wire associé à SIMD-0118 sans synthétiser une valeur `0` en cas d'omission ou de `null`.
`SolanaBlockTransaction` compose les primitives Transaction déjà acquises :
```text
SolanaEncodedTransaction
SolanaWireField<serde_json::Value> pour meta
SolanaWireField<SolanaTransactionVersion> pour version
```
Le décodeur interne `SolanaTransactionVersion::decode_wire` passe donc de privé à `pub(crate)` pour être réutilisé sans dupliquer la logique `legacy | u8`. Son comportement public reste inchangé. Une fixture `getBlock` porte volontairement la version numérique `1` afin d'empêcher une future régression qui bornerait artificiellement cette primitive à V0, conformément au watchpoint SIMD-0385.
`SolanaBlockReward` conserve :
```text
pubkey: Pubkey
lamports: i64
postBalance: u64
rewardType: String ou null
commission: u8 ou null
commissionBps: SolanaWireField<u16>
```
`commissionBps` couvre l'extension stable déjà exposée par le runtime pour SIMD-0291. `commission` et `commissionBps` restent indépendants ; Transport ne dérive jamais l'un depuis l'autre.
`rewardType` reste volontairement une chaîne nullable au niveau Transport. Cette tranche n'introduit pas une taxonomie économique KSP spéculative alors que les évolutions de reward semantics restent dans la watchlist, notamment SIMD-0123.
### Module Economics partagé
Le nouveau module privé `rpc_economics` ajoute et réexporte :
```text
SolanaInflationGovernor
SolanaInflationRate
SolanaInflationRewardConfig
SolanaInflationReward
SolanaSupplyConfig
SolanaSupply
```
`SolanaInflationGovernor` et `SolanaInflationRate` conservent directement les valeurs runtime `f64`/epoch sans reproduire localement les formules d'inflation.
`SolanaInflationRewardConfig` sérialise uniquement les options fournies :
```text
epoch
commitment
minContextSlot
```
`SolanaInflationReward` conserve `commission: Option<u8>` et `commissionBps: SolanaWireField<u16>` pour couvrir SIMD-0291 sans perdre la différence omission/null/présent du champ basis-points. La cardinalité et les `null` positionnels de la liste `getInflationReward` seront pris en charge par le wrapper de `pre.008`; le DTO de cette tranche représente uniquement une entrée non-null.
`SolanaSupplyConfig` utilise `Option<bool>` pour `excludeNonCirculatingAccountsList`. Cela distingue un champ omis d'un `false` explicitement envoyé, tout en laissant le runtime appliquer son défaut lorsque l'option est absente.
`SolanaSupply` convertit la liste ordonnée `nonCirculatingAccounts` en `Vec<Pubkey>` et échoue proprement si une clé retournée n'est pas une public key valide. Le futur wrapper réutilisera `SolanaRpcResponse<SolanaSupply>`.
### Frontières conservées
Cette prerelease n'ajoute aucun `impl HttpTransportPool` pour les 15 méthodes `V0_2_4`. Les nouveaux types sont donc uniquement des contrats wire/config/result partagés.
Le flux d'exécution reste inchangé :
```text
wrapper typed futur
-> descriptor central
-> execute_standard_rpc
-> pool/admission
-> executor HTTP
-> validation JSON-RPC
-> decode typed
```
Aucun client HTTP parallèle, aucune boucle de retry locale et aucune dépendance Solana RPC haut niveau ne sont introduits.
## Fixtures déterministes ajoutées
```text
crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json
crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json
```
Elles couvrent notamment :
- block commitment présent et `null` ;
- `getBlockProduction.byIdentity` et la range effective ;
- bloc riche avec transaction version numérique `1` ;
- `meta` explicite `null` et version omise ;
- reward `commissionBps` présent puis omis ;
- `numRewardPartitions` présent, omis et explicitement `null` ;
- performance sample courant puis ancien sans `numNonVoteTransactions` ;
- inflation governor/rate ;
- liste inflation reward avec un `null` positionnel et les variantes `commissionBps` ;
- supply avec liste ordonnée de public keys.
## Tests ajoutés
`unit_tests/rpc_blocks.rs` ajoute **8 tests déterministes** couvrant configs, résultats, omissions/null, version transaction non nulle, extensions SIMD et validation des public keys.
`unit_tests/rpc_economics.rs` ajoute **6 tests déterministes** couvrant configs, inflation values, reward basis-points, `null` positionnel de fixture, supply ordonnée et invalid pubkey.
Les tests d'intégration ajoutent aussi :
- une canarie de surface publique qui vérifie que les 17 nouveaux types sont disponibles depuis la racine de crate ;
- une canarie de registre qui confirme que `V0_2_4` reste exactement composé des **10 Blocks + 5 Economics**, tous `Read / RetrySafe`, et que `getBlock` conserve son marqueur de forme legacy dépréciée sans prétendre que les wrappers existent déjà.
La canarie typed-complete `V0_2_4 == 15` reste volontairement fermée jusqu'aux tranches d'implémentation des wrappers.
## Fichiers ajoutés
```text
crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
crates/ksp-onchain-transport-lib/src/rpc_economics.rs
crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
crates/ksp-onchain-transport-lib/fixtures/http/block_commitment.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/block_production.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/confirmed_block.omissions.json
crates/ksp-onchain-transport-lib/fixtures/http/performance_sample.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/inflation_governor.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/inflation_rate.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/inflation_reward.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/supply.v4_2_1.json
deltas/0.2.4/pre.002.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
```
## Fichiers supprimés
Aucun.
## Fichiers volontairement inchangés
```text
CHANGELOG.md
ROADMAP.md
docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md
crates/ksp-onchain-transport-lib/Cargo.toml
crates/ksp-onchain-transport-lib/README.md
crates/ksp-onchain-transport-lib/USAGE.md
crates/ksp-onchain-transport-lib/src/executor.rs
crates/ksp-onchain-transport-lib/src/rpc_method.rs
crates/ksp-config-lib/**
config/**
deltas/0.2.4/pre.001.md
deltas/0.2.4/pre.001-fix.001.md
```
Aucun ancien delta n'est réécrit. Aucun wrapper Blocks/Economics n'est avancé prématurément.
## Validations exécutées dans le sandbox
- reconstruction exacte de la base `v0.2.3` puis application des overlays `0.2.4-pre.001` et `0.2.4-pre.001-fix.001` ;
- contrôle différentiel de la tranche `pre.002` contre cette base ;
- recoupement ciblé des configs/résultats avec la baseline Agave `v4.2.1` retenue par le plan ;
- parse JSON local des neuf nouvelles fixtures ;
- contrôle statique des 17 réexports crate-root ;
- contrôle statique que chaque nouveau item `pub`/`pub(crate)` de production possède sa documentation ;
- contrôle statique de l'absence de `impl HttpTransportPool` dans les nouveaux modules ;
- contrôle statique de l'absence de `use`, `unwrap`, `expect`, `panic!` et opérateur `?` dans les deux nouvelles sources de production ;
- contrôle statique des lignes Rust modifiées à `<= 160` colonnes ;
- contrôle différentiel de `Cargo.toml` : seul `workspace.package.version` change ;
- contrôle différentiel de `rpc_transactions.rs` : seule la version de fichier et la visibilité interne documentée de `SolanaTransactionVersion::decode_wire` changent ;
- contrôle que `CHANGELOG.md`, `ROADMAP.md`, le plan `011` version 2 et les deltas historiques restent inchangés.
## Validations Cargo non exécutées
Le sandbox courant ne fournit pas `cargo`, `rustc` ni `rustfmt`. Les commandes suivantes ne sont donc pas 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
```
Aucun `cargo tree` supplémentaire n'est requis par cette tranche puisqu'aucune dépendance ni feature n'a changé. Les graphes complets restent obligatoires à la clôture `pre.009`.
Le commit attendu après application et validations suit `VER-GIT-001` :
```text
v0.2.4-pre.002
```
## Décisions prises
- séparer physiquement les primitives Blocks et Economics dans deux modules Transport dédiés ;
- ne pas avancer de wrapper avant sa tranche propriétaire ;
- réutiliser `SolanaWireField<T>`, `SolanaEncodedTransaction` et `SolanaTransactionVersion` plutôt que dupliquer le wire Transaction ;
- ouvrir uniquement la visibilité interne de `SolanaTransactionVersion::decode_wire` sans modifier son API publique ;
- typer les identités/adresses RPC en `Pubkey` lorsqu'elles sont structurelles ;
- garder les sous-arbres Transaction/meta riches lossless via `serde_json::Value` ;
- garder `rewardType` ouvert au niveau Transport au lieu d'introduire une taxonomie économique prématurée ;
- distinguer `excludeNonCirculatingAccountsList` omis d'un `false` explicite ;
- intégrer dès les fixtures partagées SIMD-0118, SIMD-0291 et la canarie future-compatible SIMD-0385 ;
- ne pas modifier le plan `011`, aucune capacité stable additive n'ayant été découverte pendant cette tranche.
## Questions ouvertes
Aucune question bloquante pour `pre.003`.
## Suite
`0.2.4-pre.003` : implémenter les cinq wrappers Blocks simples avec requêtes JSON exactes et réponses typed :
```text
getBlockCommitment
getBlockHeight
getBlockTime
getFirstAvailableBlock
minimumLedgerSlot
```