v0.2.3-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 123
|
# version: 124
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.3-pre.3"
|
version = "0.2.3-pre.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"slot":431000030,"prioritizationFee":1200},{"slot":431000029,"prioritizationFee":0}],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000052},"value":[null]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000051},"value":[]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000053},"value":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000050},"value":[null,{"slot":431000049,"confirmations":null,"status":{"Ok":null},"err":null,"confirmationStatus":"finalized"},{"slot":431000048,"confirmations":2,"status":{"Err":{"InstructionError":[0,"Custom"]}},"err":{"InstructionError":[0,"Custom"]},"confirmationStatus":"confirmed"}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction history is not available from this node"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"signature":"newest-signature","slot":431000040,"err":null,"memo":"memo-newest","blockTime":1787000100,"confirmationStatus":"finalized","transactionIndex":9},{"signature":"older-signature","slot":431000039,"err":{"InstructionError":[0,"Custom"]},"memo":null,"blockTime":null,"confirmationStatus":"confirmed"}],"id":1}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 13
|
// version: 14
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -10,9 +10,10 @@
|
|||||||
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
|
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
|
||||||
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
|
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
|
||||||
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
|
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
|
||||||
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and its first four read wrappers:
|
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and seven read wrappers through `pre.004`,
|
||||||
//! `getFeeForMessage`, `getLatestBlockhash`, `getTransactionCount` and `isBlockhashValid`. The remaining seven Transaction wrappers and the `0.2.4` family
|
//! including bounded prioritization-fee, address-signature and signature-status queries. `getTransaction`, both write submissions and
|
||||||
//! remain staged.
|
//! `simulateTransaction`, plus the
|
||||||
|
//! `0.2.4` family remain staged.
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
|
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
/// Binary encoding accepted for serialized transaction input payloads.
|
/// Binary encoding accepted for serialized transaction input payloads.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -236,14 +236,12 @@ impl SolanaSignaturesForAddressConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the pagination config would serialize to an empty object.
|
/// Returns whether the pagination config would serialize to an empty object.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn is_empty(&self) -> bool {
|
pub(crate) fn is_empty(&self) -> bool {
|
||||||
return self.before.is_none() && self.until.is_none() && self.limit.is_none() && self.commitment().is_none() && self.min_context_slot().is_none();
|
return self.before.is_none() && self.until.is_none() && self.limit.is_none() && self.commitment().is_none() && self.min_context_slot().is_none();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serializes this pagination config to the Solana JSON-RPC wire object.
|
/// Serializes this pagination config to the Solana JSON-RPC wire object.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||||
let context = self.context.to_json_value();
|
let context = self.context.to_json_value();
|
||||||
let mut object = match context {
|
let mut object = match context {
|
||||||
@@ -283,14 +281,12 @@ impl SolanaSignatureStatusesConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the status config would serialize to an empty object.
|
/// Returns whether the status config would serialize to an empty object.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) const fn is_empty(&self) -> bool {
|
pub(crate) const fn is_empty(&self) -> bool {
|
||||||
return self.search_transaction_history.is_none();
|
return self.search_transaction_history.is_none();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn to_json_value(self) -> serde_json::Value {
|
pub(crate) fn to_json_value(self) -> serde_json::Value {
|
||||||
let mut object = serde_json::Map::new();
|
let mut object = serde_json::Map::new();
|
||||||
if let std::option::Option::Some(value) = self.search_transaction_history {
|
if let std::option::Option::Some(value) = self.search_transaction_history {
|
||||||
@@ -651,7 +647,6 @@ impl SolanaPrioritizationFee {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes one prioritization-fee sample from the Solana JSON wire shape.
|
/// Decodes one prioritization-fee sample from the Solana JSON wire shape.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<WirePrioritizationFee>(method, value);
|
let decoded = crate::decode_wire_json::<WirePrioritizationFee>(method, value);
|
||||||
return match decoded {
|
return match decoded {
|
||||||
@@ -728,7 +723,6 @@ impl SolanaSignatureInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes one signature record from the Solana JSON wire shape.
|
/// Decodes one signature record from the Solana JSON wire shape.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<WireSignatureInfo>(method, value);
|
let decoded = crate::decode_wire_json::<WireSignatureInfo>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -794,7 +788,6 @@ impl SolanaSignatureStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes one present signature status from the Solana JSON wire shape.
|
/// Decodes one present signature status from the Solana JSON wire shape.
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
|
||||||
let decoded = crate::decode_wire_json::<WireSignatureStatus>(method, value);
|
let decoded = crate::decode_wire_json::<WireSignatureStatus>(method, value);
|
||||||
let wire = match decoded {
|
let wire = match decoded {
|
||||||
@@ -1102,6 +1095,10 @@ impl SolanaSimulateTransactionResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
|
||||||
|
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
|
||||||
|
const MAX_SIGNATURE_STATUSES: usize = 256;
|
||||||
|
|
||||||
impl crate::HttpTransportPool {
|
impl crate::HttpTransportPool {
|
||||||
/// Executes typed `getFeeForMessage` through the common KSP HTTP transport path.
|
/// Executes typed `getFeeForMessage` through the common KSP HTTP transport path.
|
||||||
pub async fn get_fee_for_message(
|
pub async fn get_fee_for_message(
|
||||||
@@ -1154,6 +1151,106 @@ impl crate::HttpTransportPool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getRecentPrioritizationFees` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_recent_prioritization_fees(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
writable_accounts: std::option::Option<&[ksp_core_lib::Pubkey]>,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaPrioritizationFee>> {
|
||||||
|
if let std::option::Option::Some(writable_accounts) = writable_accounts
|
||||||
|
&& writable_accounts.len() > MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS
|
||||||
|
{
|
||||||
|
return invalid_transaction_parameters(
|
||||||
|
"getRecentPrioritizationFees",
|
||||||
|
"getRecentPrioritizationFees accepts at most 128 account addresses",
|
||||||
|
"account_count",
|
||||||
|
writable_accounts.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut params = std::vec::Vec::new();
|
||||||
|
if let std::option::Option::Some(writable_accounts) = writable_accounts {
|
||||||
|
let mut addresses = std::vec::Vec::with_capacity(writable_accounts.len());
|
||||||
|
for account in writable_accounts {
|
||||||
|
addresses.push(serde_json::Value::String(account.to_string()));
|
||||||
|
}
|
||||||
|
params.push(serde_json::Value::Array(addresses));
|
||||||
|
}
|
||||||
|
let value = self.execute_transaction_rpc("getRecentPrioritizationFees", role, params).await;
|
||||||
|
let value = match value {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return decode_prioritization_fees("getRecentPrioritizationFees", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getSignaturesForAddress` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_signatures_for_address(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
address: &ksp_core_lib::Pubkey,
|
||||||
|
config: std::option::Option<&crate::SolanaSignaturesForAddressConfig>,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaSignatureInfo>> {
|
||||||
|
if let std::option::Option::Some(limit) = config.and_then(crate::SolanaSignaturesForAddressConfig::limit)
|
||||||
|
&& (limit == 0 || limit > MAX_SIGNATURES_FOR_ADDRESS_LIMIT)
|
||||||
|
{
|
||||||
|
return invalid_transaction_parameters("getSignaturesForAddress", "getSignaturesForAddress limit must be between 1 and 1000", "limit", limit);
|
||||||
|
}
|
||||||
|
let mut params = std::vec![serde_json::Value::String(address.to_string())];
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& !config.is_empty()
|
||||||
|
{
|
||||||
|
params.push(config.to_json_value());
|
||||||
|
}
|
||||||
|
let value = self.execute_transaction_rpc("getSignaturesForAddress", role, params).await;
|
||||||
|
let value = match value {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return decode_signature_infos("getSignaturesForAddress", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `getSignatureStatuses` through the common KSP HTTP transport path.
|
||||||
|
pub async fn get_signature_statuses(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
signatures: &[std::string::String],
|
||||||
|
config: std::option::Option<&crate::SolanaSignatureStatusesConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<std::vec::Vec<std::option::Option<crate::SolanaSignatureStatus>>>> {
|
||||||
|
if signatures.len() > MAX_SIGNATURE_STATUSES {
|
||||||
|
return invalid_transaction_parameters(
|
||||||
|
"getSignatureStatuses",
|
||||||
|
"getSignatureStatuses accepts at most 256 signatures",
|
||||||
|
"signature_count",
|
||||||
|
signatures.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut signature_values = std::vec::Vec::with_capacity(signatures.len());
|
||||||
|
for signature in signatures {
|
||||||
|
signature_values.push(serde_json::Value::String(signature.clone()));
|
||||||
|
}
|
||||||
|
let mut params = std::vec![serde_json::Value::Array(signature_values)];
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& !config.is_empty()
|
||||||
|
{
|
||||||
|
params.push((*config).to_json_value());
|
||||||
|
}
|
||||||
|
let value = self.execute_transaction_rpc("getSignatureStatuses", role, params).await;
|
||||||
|
let value = match value {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let contextual = decode_transaction_contextual_wire("getSignatureStatuses", value);
|
||||||
|
let (context, value) = match contextual {
|
||||||
|
std::result::Result::Ok(contextual) => contextual,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let statuses = decode_signature_statuses("getSignatureStatuses", value, signatures.len());
|
||||||
|
return match statuses {
|
||||||
|
std::result::Result::Ok(statuses) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, statuses)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes typed `getTransactionCount` through the common KSP HTTP transport path.
|
/// Executes typed `getTransactionCount` through the common KSP HTTP transport path.
|
||||||
pub async fn get_transaction_count(
|
pub async fn get_transaction_count(
|
||||||
&self,
|
&self,
|
||||||
@@ -1232,6 +1329,84 @@ fn decode_transaction_contextual_wire(method: &str, value: serde_json::Value) ->
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_prioritization_fees(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaPrioritizationFee>> {
|
||||||
|
let decoded = crate::decode_wire_json::<std::vec::Vec<serde_json::Value>>(method, value);
|
||||||
|
let values = match decoded {
|
||||||
|
std::result::Result::Ok(values) => values,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut fees = std::vec::Vec::with_capacity(values.len());
|
||||||
|
for value in values {
|
||||||
|
let fee = crate::SolanaPrioritizationFee::decode_wire(method, value);
|
||||||
|
match fee {
|
||||||
|
std::result::Result::Ok(fee) => fees.push(fee),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(fees);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_signature_infos(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<crate::SolanaSignatureInfo>> {
|
||||||
|
let decoded = crate::decode_wire_json::<std::vec::Vec<serde_json::Value>>(method, value);
|
||||||
|
let values = match decoded {
|
||||||
|
std::result::Result::Ok(values) => values,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let mut infos = std::vec::Vec::with_capacity(values.len());
|
||||||
|
for value in values {
|
||||||
|
let info = crate::SolanaSignatureInfo::decode_wire(method, value);
|
||||||
|
match info {
|
||||||
|
std::result::Result::Ok(info) => infos.push(info),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(infos);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_signature_statuses(
|
||||||
|
method: &str,
|
||||||
|
value: serde_json::Value,
|
||||||
|
expected_count: usize,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<std::option::Option<crate::SolanaSignatureStatus>>> {
|
||||||
|
let decoded = crate::decode_wire_json::<std::vec::Vec<std::option::Option<serde_json::Value>>>(method, value);
|
||||||
|
let values = match decoded {
|
||||||
|
std::result::Result::Ok(values) => values,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if values.len() != expected_count {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "getSignatureStatuses result count does not match the requested signature count")
|
||||||
|
.with_context("rpc_method", method)
|
||||||
|
.with_context("expected_count", expected_count.to_string())
|
||||||
|
.with_context("actual_count", values.len().to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut statuses = std::vec::Vec::with_capacity(values.len());
|
||||||
|
for value in values {
|
||||||
|
let value = match value {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
statuses.push(std::option::Option::None);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let status = crate::SolanaSignatureStatus::decode_wire(method, value);
|
||||||
|
match status {
|
||||||
|
std::result::Result::Ok(status) => statuses.push(std::option::Option::Some(status)),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_transaction_parameters<T>(method: &str, message: &str, field: &'static str, value: usize) -> ksp_core_lib::Result<T> {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
|
||||||
|
.with_context("rpc_method", method)
|
||||||
|
.with_context(field, value.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn transaction_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
fn transaction_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||||
let descriptor = crate::find_http_rpc_method(method);
|
let descriptor = crate::find_http_rpc_method(method);
|
||||||
return match descriptor {
|
return match descriptor {
|
||||||
@@ -1257,7 +1432,6 @@ fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) ->
|
|||||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field);
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn decode_confirmation_status(
|
fn decode_confirmation_status(
|
||||||
method: &str,
|
method: &str,
|
||||||
field: &str,
|
field: &str,
|
||||||
@@ -1386,7 +1560,6 @@ struct WireLatestBlockhash {
|
|||||||
last_valid_block_height: u64,
|
last_valid_block_height: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct WirePrioritizationFee {
|
struct WirePrioritizationFee {
|
||||||
@@ -1394,7 +1567,6 @@ struct WirePrioritizationFee {
|
|||||||
prioritization_fee: u64,
|
prioritization_fee: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct WireSignatureInfo {
|
struct WireSignatureInfo {
|
||||||
@@ -1412,7 +1584,6 @@ struct WireSignatureInfo {
|
|||||||
transaction_index: std::option::Option<u32>,
|
transaction_index: std::option::Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct WireSignatureStatus {
|
struct WireSignatureStatus {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
// version: 11
|
// version: 12
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -278,3 +278,10 @@ fn public_transaction_pre_003_read_wrappers_are_available_from_crate_root() {
|
|||||||
let _get_transaction_count = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_count;
|
let _get_transaction_count = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_count;
|
||||||
let _is_blockhash_valid = ksp_onchain_transport_lib::HttpTransportPool::is_blockhash_valid;
|
let _is_blockhash_valid = ksp_onchain_transport_lib::HttpTransportPool::is_blockhash_valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_transaction_pre_004_additional_read_wrappers_are_available_from_crate_root() {
|
||||||
|
let _get_recent_prioritization_fees = ksp_onchain_transport_lib::HttpTransportPool::get_recent_prioritization_fees;
|
||||||
|
let _get_signatures_for_address = ksp_onchain_transport_lib::HttpTransportPool::get_signatures_for_address;
|
||||||
|
let _get_signature_statuses = ksp_onchain_transport_lib::HttpTransportPool::get_signature_statuses;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||||
|
|
||||||
@@ -259,3 +259,35 @@ fn release_pre_003_transaction_read_subset_is_exact_and_retry_safe() {
|
|||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
assert_eq!(actual.len(), 4);
|
assert_eq!(actual.len(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_pre_004_transaction_read_subset_is_exact_and_retry_safe() {
|
||||||
|
let mut expected = std::vec![
|
||||||
|
"getFeeForMessage",
|
||||||
|
"getLatestBlockhash",
|
||||||
|
"getRecentPrioritizationFees",
|
||||||
|
"getSignaturesForAddress",
|
||||||
|
"getSignatureStatuses",
|
||||||
|
"getTransactionCount",
|
||||||
|
"isBlockhashValid",
|
||||||
|
];
|
||||||
|
let deferred = std::vec!["getTransaction", "requestAirdrop", "sendTransaction", "simulateTransaction"];
|
||||||
|
let mut actual = std::vec::Vec::new();
|
||||||
|
for method_name in &expected {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.004 Transaction descriptor must exist");
|
||||||
|
actual.push(descriptor.method());
|
||||||
|
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
|
||||||
|
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||||
|
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
|
||||||
|
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
|
||||||
|
}
|
||||||
|
for method_name in &deferred {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("deferred Transaction descriptor must remain registered");
|
||||||
|
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
|
||||||
|
}
|
||||||
|
actual.sort_unstable();
|
||||||
|
expected.sort_unstable();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
assert_eq!(actual.len(), 7);
|
||||||
|
assert_eq!(deferred.len(), 4);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
||||||
@@ -470,3 +470,219 @@ async fn typed_is_blockhash_valid_preserves_opaque_blockhash_and_contextual_bool
|
|||||||
assert_eq!(body["method"], serde_json::json!("isBlockhashValid"));
|
assert_eq!(body["method"], serde_json::json!("isBlockhashValid"));
|
||||||
assert_eq!(body["params"], serde_json::json!(["opaque-blockhash", {"commitment":"processed","minContextSlot":431000004}]));
|
assert_eq!(body["params"], serde_json::json!(["opaque-blockhash", {"commitment":"processed","minContextSlot":431000004}]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_recent_prioritization_fees_serializes_addresses_and_preserves_server_order() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let first = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let second = "SysvarRent111111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let first_text = first.to_string();
|
||||||
|
let second_text = second.to_string();
|
||||||
|
let accounts = std::vec![first, second];
|
||||||
|
let fees = pool
|
||||||
|
.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice()))
|
||||||
|
.await
|
||||||
|
.expect("getRecentPrioritizationFees fixture must succeed");
|
||||||
|
assert_eq!(fees.len(), 2);
|
||||||
|
assert_eq!(fees[0].slot(), 431_000_030);
|
||||||
|
assert_eq!(fees[0].prioritization_fee(), 1_200);
|
||||||
|
assert_eq!(fees[1].slot(), 431_000_029);
|
||||||
|
assert_eq!(fees[1].prioritization_fee(), 0);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("getRecentPrioritizationFees"));
|
||||||
|
assert_eq!(body["params"], serde_json::json!([[first_text, second_text]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_rejects_more_than_128_before_io() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let fees = pool
|
||||||
|
.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("getRecentPrioritizationFees without addresses must succeed");
|
||||||
|
assert_eq!(fees.len(), 2);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([]));
|
||||||
|
|
||||||
|
let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let max_accounts = std::vec![account; 128];
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
|
||||||
|
let max_pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let fees = max_pool
|
||||||
|
.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(max_accounts.as_slice()))
|
||||||
|
.await
|
||||||
|
.expect("exactly 128 prioritization-fee accounts must remain accepted");
|
||||||
|
assert_eq!(fees.len(), 2);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["params"][0].as_array().expect("address parameter must be an array").len(), 128);
|
||||||
|
|
||||||
|
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
|
||||||
|
let accounts = std::vec![account; 129];
|
||||||
|
let result = invalid_pool.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice())).await;
|
||||||
|
let error = result.expect_err("more than 128 prioritization-fee accounts must reject before I/O");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
assert_eq!(error.context()[1].key(), "account_count");
|
||||||
|
assert_eq!(error.context()[1].value(), "129");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signatures_for_address_serializes_pagination_and_preserves_newest_first_result() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signatures_for_address.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let config = crate::SolanaSignaturesForAddressConfig::new(
|
||||||
|
std::option::Option::Some("before-signature".to_owned()),
|
||||||
|
std::option::Option::Some("until-signature".to_owned()),
|
||||||
|
std::option::Option::Some(1_000),
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||||
|
std::option::Option::Some(431_000_000),
|
||||||
|
);
|
||||||
|
let signatures = pool
|
||||||
|
.get_signatures_for_address(&crate::HttpRoleName::new("default"), &address, std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("getSignaturesForAddress fixture must succeed");
|
||||||
|
assert_eq!(signatures.len(), 2);
|
||||||
|
assert_eq!(signatures[0].signature(), "newest-signature");
|
||||||
|
assert_eq!(signatures[0].slot(), 431_000_040);
|
||||||
|
assert_eq!(signatures[0].memo(), std::option::Option::Some("memo-newest"));
|
||||||
|
assert_eq!(signatures[0].block_time(), std::option::Option::Some(1_787_000_100));
|
||||||
|
assert_eq!(signatures[0].confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Finalized));
|
||||||
|
assert_eq!(signatures[0].transaction_index(), std::option::Option::Some(9));
|
||||||
|
assert_eq!(signatures[1].signature(), "older-signature");
|
||||||
|
assert!(signatures[1].err().is_some());
|
||||||
|
assert_eq!(signatures[1].memo(), std::option::Option::None);
|
||||||
|
assert_eq!(signatures[1].block_time(), std::option::Option::None);
|
||||||
|
assert_eq!(signatures[1].transaction_index(), std::option::Option::None);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("getSignaturesForAddress"));
|
||||||
|
assert_eq!(
|
||||||
|
body["params"],
|
||||||
|
serde_json::json!([
|
||||||
|
address.to_string(),
|
||||||
|
{
|
||||||
|
"before":"before-signature",
|
||||||
|
"until":"until-signature",
|
||||||
|
"limit":1000,
|
||||||
|
"commitment":"finalized",
|
||||||
|
"minContextSlot":431000000
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signatures_for_address_rejects_limit_outside_1_to_1000_before_io() {
|
||||||
|
let pool = transaction_pool_for_url("http://127.0.0.1:9");
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
for limit in [0_usize, 1_001_usize] {
|
||||||
|
let config = crate::SolanaSignaturesForAddressConfig::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(limit),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let result = pool.get_signatures_for_address(&crate::HttpRoleName::new("default"), &address, std::option::Option::Some(&config)).await;
|
||||||
|
let error = result.expect_err("out-of-range getSignaturesForAddress limit must reject before I/O");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
assert_eq!(error.context()[1].key(), "limit");
|
||||||
|
assert_eq!(error.context()[1].value(), limit.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signatures_for_address_preserves_rpc_application_error() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signatures_for_address.error.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let result = pool.get_signatures_for_address(&crate::HttpRoleName::new("default"), &address, std::option::Option::None).await;
|
||||||
|
let error = result.expect_err("remote transaction-history error must remain an RPC application error");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signature_statuses_preserves_positional_nulls_and_history_config() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let signatures = std::vec!["missing-signature".to_owned(), "finalized-signature".to_owned(), "confirmed-signature".to_owned()];
|
||||||
|
let config = crate::SolanaSignatureStatusesConfig::new(std::option::Option::Some(true));
|
||||||
|
let response = pool
|
||||||
|
.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("getSignatureStatuses fixture must succeed");
|
||||||
|
assert_eq!(response.context().slot(), 431_000_050);
|
||||||
|
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
|
||||||
|
assert_eq!(response.value().len(), 3);
|
||||||
|
assert!(response.value()[0].is_none());
|
||||||
|
let finalized = response.value()[1].as_ref().expect("second positional status must be present");
|
||||||
|
assert_eq!(finalized.slot(), 431_000_049);
|
||||||
|
assert_eq!(finalized.confirmations(), std::option::Option::None);
|
||||||
|
assert_eq!(finalized.status(), &serde_json::json!({"Ok":null}));
|
||||||
|
assert_eq!(finalized.confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Finalized));
|
||||||
|
let confirmed = response.value()[2].as_ref().expect("third positional status must be present");
|
||||||
|
assert_eq!(confirmed.confirmations(), std::option::Option::Some(2));
|
||||||
|
assert!(confirmed.err().is_some());
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("getSignatureStatuses"));
|
||||||
|
assert_eq!(body["params"], serde_json::json!([signatures, {"searchTransactionHistory":true}]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signature_statuses_accepts_empty_input_and_omits_empty_config() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.empty.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let signatures = std::vec::Vec::<std::string::String>::new();
|
||||||
|
let config = crate::SolanaSignatureStatusesConfig::default();
|
||||||
|
let response = pool
|
||||||
|
.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("empty getSignatureStatuses input must remain accepted");
|
||||||
|
assert!(response.value().is_empty());
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([[]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signature_statuses_accepts_256_and_rejects_more_than_256_before_io() {
|
||||||
|
let signatures = std::vec!["signature".to_owned(); 256];
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.max_256.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let response = pool
|
||||||
|
.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("exactly 256 signatures must remain accepted");
|
||||||
|
assert_eq!(response.value().len(), 256);
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["params"][0].as_array().expect("signature parameter must be an array").len(), 256);
|
||||||
|
|
||||||
|
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
|
||||||
|
let signatures = std::vec!["signature".to_owned(); 257];
|
||||||
|
let result = invalid_pool.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None).await;
|
||||||
|
let error = result.expect_err("more than 256 signatures must reject before I/O");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
assert_eq!(error.context()[1].key(), "signature_count");
|
||||||
|
assert_eq!(error.context()[1].value(), "257");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_signature_statuses_rejects_response_count_mismatch() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.count_mismatch.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let requested = std::vec!["first".to_owned(), "second".to_owned()];
|
||||||
|
let result = pool.get_signature_statuses(&crate::HttpRoleName::new("default"), requested.as_slice(), std::option::Option::None).await;
|
||||||
|
let error = result.expect_err("status response count mismatch must reject typed decoding");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
|
||||||
|
assert_eq!(error.context()[1].key(), "expected_count");
|
||||||
|
assert_eq!(error.context()[1].value(), "2");
|
||||||
|
assert_eq!(error.context()[2].key(), "actual_count");
|
||||||
|
assert_eq!(error.context()[2].value(), "1");
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|||||||
346
deltas/0.2.3/pre.004.md
Normal file
346
deltas/0.2.3/pre.004.md
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
<!-- file: deltas/0.2.3/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.3-pre.004` — lectures Transaction avec cardinalités et statuts positionnels
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée localement par l'opérateur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.003
|
||||||
|
workspace.package.version = "0.2.3-pre.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation opérateur du 2026-08-18 a confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats Transport de cette base :
|
||||||
|
|
||||||
|
```text
|
||||||
|
152 unit tests
|
||||||
|
15 public API tests
|
||||||
|
9 release completeness tests
|
||||||
|
0 warning signalé par check/clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
Le plan canonique reste `docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md` version 3.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Implémenter les trois wrappers Transaction `Read / RetrySafe` prévus par la tranche `pre.004` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
getRecentPrioritizationFees
|
||||||
|
getSignaturesForAddress
|
||||||
|
getSignatureStatuses
|
||||||
|
```
|
||||||
|
|
||||||
|
Ils rejoignent les quatre reads de `pre.003`, portant la surface Transaction typée exécutée à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
7 / 11 wrappers
|
||||||
|
```
|
||||||
|
|
||||||
|
Les quatre méthodes restantes restent volontairement différées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
getTransaction
|
||||||
|
requestAirdrop
|
||||||
|
sendTransaction
|
||||||
|
simulateTransaction
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Conformément à `VER-ID-009` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.3 -> 0.2.3-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n'est ajoutée.
|
||||||
|
|
||||||
|
## `getRecentPrioritizationFees`
|
||||||
|
|
||||||
|
Surface publique conceptuelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + Option<&[Pubkey]>
|
||||||
|
-> Vec<SolanaPrioritizationFee>
|
||||||
|
```
|
||||||
|
|
||||||
|
Le tableau d'adresses reste optionnel :
|
||||||
|
|
||||||
|
- `None` omet entièrement le paramètre ;
|
||||||
|
- `Some(slice)` sérialise exactement un tableau d'adresses ;
|
||||||
|
- l'ordre des adresses caller est conservé ;
|
||||||
|
- les résultats restent dans l'ordre renvoyé par le provider.
|
||||||
|
|
||||||
|
La cardinalité runtime documentée est appliquée avant I/O :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0..=128 accepté
|
||||||
|
129+ ERROR_CODE_INVALID_RPC_PARAMETERS
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test couvre explicitement la borne `128` puis le rejet de `129`.
|
||||||
|
|
||||||
|
Aucune règle locale n'est créée à partir de la profondeur actuelle de cache des prioritization fees : la valeur d'environ 150 blocs est une
|
||||||
|
caractéristique runtime du noeud et non une cardinalité client.
|
||||||
|
|
||||||
|
## `getSignaturesForAddress`
|
||||||
|
|
||||||
|
Surface publique conceptuelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + Pubkey + Option<SolanaSignaturesForAddressConfig>
|
||||||
|
-> Vec<SolanaSignatureInfo>
|
||||||
|
```
|
||||||
|
|
||||||
|
La config préparée en `pre.002` devient production-live pour cette méthode :
|
||||||
|
|
||||||
|
```text
|
||||||
|
before
|
||||||
|
until
|
||||||
|
limit
|
||||||
|
commitment
|
||||||
|
minContextSlot
|
||||||
|
```
|
||||||
|
|
||||||
|
Une config absente ou entièrement vide est omise. La limite est validée avant I/O exactement selon le runtime audité :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1..=1000 accepté quand limit est fourni
|
||||||
|
0 rejeté
|
||||||
|
1001+ rejeté
|
||||||
|
```
|
||||||
|
|
||||||
|
La fixture de succès utilise explicitement `limit = 1000`, puis les tests rejettent `0` et `1001`.
|
||||||
|
|
||||||
|
Le résultat conserve notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
signature
|
||||||
|
slot
|
||||||
|
err nullable
|
||||||
|
memo nullable
|
||||||
|
blockTime nullable
|
||||||
|
confirmationStatus nullable
|
||||||
|
transactionIndex optionnel
|
||||||
|
```
|
||||||
|
|
||||||
|
L'ordre `newest -> oldest` du provider n'est jamais retrié côté KSP. Les signatures restent des chaînes wire opaques dans cette release.
|
||||||
|
|
||||||
|
Une fixture JSON-RPC d'erreur vérifie qu'une indisponibilité de transaction history reste une `RPC_APPLICATION_ERROR`, sans être transformée en
|
||||||
|
retry Transport.
|
||||||
|
|
||||||
|
## `getSignatureStatuses`
|
||||||
|
|
||||||
|
Surface publique conceptuelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
role + &[String] + Option<SolanaSignatureStatusesConfig>
|
||||||
|
-> SolanaRpcResponse<Vec<Option<SolanaSignatureStatus>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
La config `searchTransactionHistory` préparée en `pre.002` devient production-live. Une config vide est omise.
|
||||||
|
|
||||||
|
Le tableau d'entrée suit exactement la borne Agave auditée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0..=256 accepté
|
||||||
|
257+ ERROR_CODE_INVALID_RPC_PARAMETERS
|
||||||
|
```
|
||||||
|
|
||||||
|
Le tableau vide reste volontairement accepté, conformément au runtime `v4.2.1` audité en `pre.001`. La borne `256` est exercée par une fixture
|
||||||
|
contenant 256 positions `null` et `257` est rejeté avant I/O.
|
||||||
|
|
||||||
|
Le résultat conserve strictement la correspondance positionnelle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
entrée[i] <-> value[i]
|
||||||
|
null -> None
|
||||||
|
objet -> Some(SolanaSignatureStatus)
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP vérifie maintenant que le provider retourne exactement autant de positions que de signatures demandées. Une longueur différente produit
|
||||||
|
`ERROR_CODE_INVALID_RESPONSE`, car continuer masquerait la correspondance signature/statut.
|
||||||
|
|
||||||
|
Pour un statut présent, les champs suivants restent préservés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
slot
|
||||||
|
confirmations nullable
|
||||||
|
status legacy lossless JSON
|
||||||
|
err nullable
|
||||||
|
confirmationStatus nullable
|
||||||
|
```
|
||||||
|
|
||||||
|
La forme `status` demeure un champ legacy du wire courant ; elle n'est pas confondue avec les anciennes méthodes RPC Deprecated.
|
||||||
|
|
||||||
|
## Helpers activés en production
|
||||||
|
|
||||||
|
Seuls les helpers nécessaires à cette tranche quittent `#[cfg(test)]` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaSignaturesForAddressConfig::{is_empty,to_json_value}
|
||||||
|
SolanaSignatureStatusesConfig::{is_empty,to_json_value}
|
||||||
|
SolanaPrioritizationFee::decode_wire
|
||||||
|
SolanaSignatureInfo::decode_wire
|
||||||
|
SolanaSignatureStatus::decode_wire
|
||||||
|
decode_confirmation_status
|
||||||
|
WirePrioritizationFee
|
||||||
|
WireSignatureInfo
|
||||||
|
WireSignatureStatus
|
||||||
|
```
|
||||||
|
|
||||||
|
Les helpers de `getTransaction`, `requestAirdrop`, `sendTransaction` et `simulateTransaction` restent staged/test-only jusqu'à leur tranche
|
||||||
|
propriétaire. Aucun `#[allow(dead_code)]` n'est introduit.
|
||||||
|
|
||||||
|
## Fixtures HTTP déterministes ajoutées
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_recent_prioritization_fees.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signatures_for_address.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signatures_for_address.error.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.empty.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.max_256.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.count_mismatch.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests HTTP utilisent uniquement un serveur loopback local.
|
||||||
|
|
||||||
|
## Couverture de tests ajoutée
|
||||||
|
|
||||||
|
Neuf tests unitaires HTTP supplémentaires couvrent :
|
||||||
|
|
||||||
|
- paramètres exacts de `getRecentPrioritizationFees` et ordre serveur ;
|
||||||
|
- omission du paramètre optionnel ;
|
||||||
|
- acceptation de 128 adresses et rejet local de 129 ;
|
||||||
|
- pagination/config complète de `getSignaturesForAddress` avec `limit = 1000` ;
|
||||||
|
- rejet local de `limit = 0` et `limit = 1001` ;
|
||||||
|
- propagation d'une erreur JSON-RPC applicative ;
|
||||||
|
- `getSignatureStatuses` avec `searchTransactionHistory = true` et positions `null` ;
|
||||||
|
- tableau vide + config vide omise ;
|
||||||
|
- borne 256 acceptée, 257 rejetée et longueur de réponse incohérente rejetée.
|
||||||
|
|
||||||
|
Un test public compile les trois nouveaux wrappers depuis la crate root.
|
||||||
|
|
||||||
|
Une nouvelle canarie release fige le sous-ensemble Read exécuté cumulé de `pre.003 + pre.004` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
getFeeForMessage
|
||||||
|
getLatestBlockhash
|
||||||
|
getRecentPrioritizationFees
|
||||||
|
getSignaturesForAddress
|
||||||
|
getSignatureStatuses
|
||||||
|
getTransactionCount
|
||||||
|
isBlockhashValid
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque descriptor reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transactions / V0_2_3 / Read / RetrySafe
|
||||||
|
```
|
||||||
|
|
||||||
|
La canarie confirme aussi que les quatre méthodes différées restent enregistrées dans `V0_2_3` sans les compter comme surface typée achevée.
|
||||||
|
|
||||||
|
Après application, la cible Transport attendue devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
161 unit tests
|
||||||
|
16 public API tests
|
||||||
|
10 release completeness tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_recent_prioritization_fees.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signatures_for_address.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signatures_for_address.error.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.empty.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.max_256.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/get_signature_statuses.count_mismatch.json
|
||||||
|
deltas/0.2.3/pre.004.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/unit_tests/rpc_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Restent inchangés dans cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md
|
||||||
|
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/executor.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/resilience.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contrôles statiques effectués dans l'environnement d'échange
|
||||||
|
|
||||||
|
- les sept nouvelles fixtures sont du JSON valide ;
|
||||||
|
- aucune dépendance n'est ajoutée ;
|
||||||
|
- aucun `reqwest`, `tracing`, retry local ou client parallèle n'est introduit par le diff ;
|
||||||
|
- aucun `#[allow(dead_code)]` n'est introduit ;
|
||||||
|
- les helpers des tranches futures restent `#[cfg(test)]` ;
|
||||||
|
- aucune ligne Rust modifiée ne dépasse 160 colonnes ;
|
||||||
|
- `CHANGELOG.md`, `ROADMAP.md`, le plan `010`, `executor.rs`, `resilience.rs` et `rpc_method.rs` restent identiques à `pre.003`.
|
||||||
|
|
||||||
|
`cargo`, `rustc` et `rustfmt` ne sont pas installés dans l'environnement d'échange. Aucune validation Cargo de `pre.004` n'est donc déclarée
|
||||||
|
réussie ici.
|
||||||
|
|
||||||
|
## Validation opérateur requise
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation doit notamment confirmer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 warning check/clippy
|
||||||
|
161 unit tests
|
||||||
|
16 public API tests
|
||||||
|
10 release completeness tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke Devnet reste opt-in et n'est pas requis pour cette tranche locale déterministe.
|
||||||
|
|
||||||
|
## Hors scope maintenu
|
||||||
|
|
||||||
|
`pre.004` ne traite pas :
|
||||||
|
|
||||||
|
- `getTransaction` moderne/legacy ;
|
||||||
|
- `requestAirdrop` ;
|
||||||
|
- `sendTransaction` ;
|
||||||
|
- la preuve end-to-end no-resend des writes ;
|
||||||
|
- `simulateTransaction` ;
|
||||||
|
- les 15 méthodes `0.2.4` Blocks/Economics ;
|
||||||
|
- un nouveau smoke cross-crates.
|
||||||
|
|
||||||
|
La prochaine tranche reste `pre.005` : `getTransaction` moderne, compatibilité legacy et wire transaction/meta/version.
|
||||||
Reference in New Issue
Block a user