v0.3.6-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 398
|
# version: 399
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.6-pre.3.fix.1"
|
version = "0.3.6-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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/http_executor.rs
|
// file: crates/ksp-onchain-transport-lib/src/http_executor.rs
|
||||||
// version: 4
|
// version: 7
|
||||||
|
|
||||||
const HTTP_BAD_GATEWAY: u16 = 502;
|
const HTTP_BAD_GATEWAY: u16 = 502;
|
||||||
const HTTP_GATEWAY_TIMEOUT: u16 = 504;
|
const HTTP_GATEWAY_TIMEOUT: u16 = 504;
|
||||||
@@ -8,6 +8,63 @@ const HTTP_REQUEST_TIMEOUT: u16 = 408;
|
|||||||
const HTTP_SERVICE_UNAVAILABLE: u16 = 503;
|
const HTTP_SERVICE_UNAVAILABLE: u16 = 503;
|
||||||
const HTTP_TOO_MANY_REQUESTS: u16 = 429;
|
const HTTP_TOO_MANY_REQUESTS: u16 = 429;
|
||||||
|
|
||||||
|
/// Typed value returned by an observed HTTP RPC path together with the safe identity of the endpoint that produced the successful response.
|
||||||
|
///
|
||||||
|
/// Endpoint URLs, headers and raw HTTP bodies are intentionally absent.
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct HttpObservedValue<T> {
|
||||||
|
value: T,
|
||||||
|
endpoint_name: std::string::String,
|
||||||
|
provider: crate::HttpProviderName,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> HttpObservedValue<T> {
|
||||||
|
/// Returns the typed RPC value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn value(&self) -> &T {
|
||||||
|
return &self.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe configured identity of the endpoint that produced the successful response.
|
||||||
|
#[must_use]
|
||||||
|
pub fn endpoint_name(&self) -> &str {
|
||||||
|
return self.endpoint_name.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe provider descriptor attached to the successful endpoint.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn provider(&self) -> &crate::HttpProviderName {
|
||||||
|
return &self.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the observation and returns only the typed value.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_value(self) -> T {
|
||||||
|
return self.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an observed value from a successful Transport attempt and its safe routing identity.
|
||||||
|
pub(crate) fn new(value: T, endpoint_name: std::string::String, provider: crate::HttpProviderName) -> Self {
|
||||||
|
return Self { value, endpoint_name, provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the observation into its typed value and safe routing identity for crate-internal typed decoding.
|
||||||
|
pub(crate) fn into_parts(self) -> (T, std::string::String, crate::HttpProviderName) {
|
||||||
|
return (self.value, self.endpoint_name, self.provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Debug for HttpObservedValue<T> {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("HttpObservedValue")
|
||||||
|
.field("endpoint_name", &self.endpoint_name)
|
||||||
|
.field("provider", &self.provider)
|
||||||
|
.field("value", &"<available>")
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl crate::HttpTransportPool {
|
impl crate::HttpTransportPool {
|
||||||
/// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy.
|
/// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy.
|
||||||
///
|
///
|
||||||
@@ -19,6 +76,33 @@ impl crate::HttpTransportPool {
|
|||||||
method: &crate::HttpRpcMethodDescriptor,
|
method: &crate::HttpRpcMethodDescriptor,
|
||||||
params: std::vec::Vec<serde_json::Value>,
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
) -> ksp_core_lib::Result<serde_json::Value> {
|
) -> ksp_core_lib::Result<serde_json::Value> {
|
||||||
|
return self.execute_standard_rpc_with(role, method, params, |value, _permit| return value).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes one audited standard Solana HTTP JSON-RPC method and retains only safe routing identity for the successful attempt.
|
||||||
|
pub(crate) async fn execute_standard_rpc_observed(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
method: &crate::HttpRpcMethodDescriptor,
|
||||||
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
|
) -> ksp_core_lib::Result<crate::HttpObservedValue<serde_json::Value>> {
|
||||||
|
return self
|
||||||
|
.execute_standard_rpc_with(role, method, params, |value, permit| {
|
||||||
|
return crate::HttpObservedValue::new(value, permit.selection().endpoint_name().to_owned(), permit.client().provider().clone());
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_standard_rpc_with<T, F>(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
method: &crate::HttpRpcMethodDescriptor,
|
||||||
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
|
on_success: F,
|
||||||
|
) -> ksp_core_lib::Result<T>
|
||||||
|
where
|
||||||
|
F: std::ops::FnOnce(serde_json::Value, &crate::HttpRequestPermit) -> T,
|
||||||
|
{
|
||||||
let support = method.ensure_runtime_supported();
|
let support = method.ensure_runtime_supported();
|
||||||
if let std::result::Result::Err(error) = support {
|
if let std::result::Result::Err(error) = support {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
@@ -156,7 +240,12 @@ impl crate::HttpTransportPool {
|
|||||||
http_status = status,
|
http_status = status,
|
||||||
"completed Solana HTTP JSON-RPC request"
|
"completed Solana HTTP JSON-RPC request"
|
||||||
);
|
);
|
||||||
return parsed.into_result();
|
let value = parsed.into_result();
|
||||||
|
let value = match value {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(on_success(value, &permit));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,14 +292,11 @@ async fn wait_retry_delay(delay: std::time::Duration, deadline: std::time::Insta
|
|||||||
return std::time::Instant::now() < deadline;
|
return std::time::Instant::now() < deadline;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execution_timeout(method: &crate::HttpRpcMethodDescriptor, message: &str) -> ksp_core_lib::Result<serde_json::Value> {
|
fn execution_timeout<T>(method: &crate::HttpRpcMethodDescriptor, message: &str) -> ksp_core_lib::Result<T> {
|
||||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, message).with_context("rpc_method", method.method()));
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, message).with_context("rpc_method", method.method()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rate_limited_error(
|
fn rate_limited_error<T>(method: &crate::HttpRpcMethodDescriptor, provider_retry_after: std::option::Option<std::time::Duration>) -> ksp_core_lib::Result<T> {
|
||||||
method: &crate::HttpRpcMethodDescriptor,
|
|
||||||
provider_retry_after: std::option::Option<std::time::Duration>,
|
|
||||||
) -> ksp_core_lib::Result<serde_json::Value> {
|
|
||||||
let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_RATE_LIMITED, "Solana HTTP endpoint rate-limited the JSON-RPC request")
|
let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_RATE_LIMITED, "Solana HTTP endpoint rate-limited the JSON-RPC request")
|
||||||
.with_context("rpc_method", method.method());
|
.with_context("rpc_method", method.method());
|
||||||
if let std::option::Option::Some(delay) = provider_retry_after {
|
if let std::option::Option::Some(delay) = provider_retry_after {
|
||||||
@@ -219,7 +305,7 @@ fn rate_limited_error(
|
|||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn http_status_error(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ksp_core_lib::Result<serde_json::Value> {
|
fn http_status_error<T>(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ksp_core_lib::Result<T> {
|
||||||
return std::result::Result::Err(
|
return std::result::Result::Err(
|
||||||
ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_FAILED, "Solana HTTP endpoint returned an unsuccessful status")
|
ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_FAILED, "Solana HTTP endpoint returned an unsuccessful status")
|
||||||
.with_context("rpc_method", method.method())
|
.with_context("rpc_method", method.method())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 44
|
// version: 45
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -271,6 +271,8 @@ pub use self::http_client::HttpEndpointClient;
|
|||||||
pub use self::http_client::HttpEndpointRoleSnapshot;
|
pub use self::http_client::HttpEndpointRoleSnapshot;
|
||||||
/// Safe metadata snapshot for one logical HTTP endpoint.
|
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||||
pub use self::http_client::HttpEndpointSnapshot;
|
pub use self::http_client::HttpEndpointSnapshot;
|
||||||
|
/// Typed RPC value paired with the safe identity of the HTTP endpoint that produced the successful response.
|
||||||
|
pub use self::http_executor::HttpObservedValue;
|
||||||
/// Result of one logical endpoint selection.
|
/// Result of one logical endpoint selection.
|
||||||
pub use self::http_pool::HttpEndpointSelection;
|
pub use self::http_pool::HttpEndpointSelection;
|
||||||
/// Runtime admission permit for one HTTP request.
|
/// Runtime admission permit for one HTTP request.
|
||||||
|
|||||||
@@ -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: 10
|
// version: 11
|
||||||
|
|
||||||
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
|
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
|
||||||
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
|
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
|
||||||
@@ -1249,25 +1249,42 @@ impl crate::HttpTransportPool {
|
|||||||
signature: &str,
|
signature: &str,
|
||||||
config: std::option::Option<&crate::SolanaGetTransactionConfig>,
|
config: std::option::Option<&crate::SolanaGetTransactionConfig>,
|
||||||
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedTransaction>> {
|
) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedTransaction>> {
|
||||||
if let std::option::Option::Some(config) = config
|
let params = get_transaction_params(signature, config);
|
||||||
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
let params = match params {
|
||||||
{
|
std::result::Result::Ok(params) => params,
|
||||||
return std::result::Result::Err(
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
ksp_core_lib::Error::new(
|
};
|
||||||
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
|
||||||
"getTransaction commitment must be confirmed or finalized when explicitly provided",
|
|
||||||
)
|
|
||||||
.with_context("rpc_method", "getTransaction")
|
|
||||||
.with_context("commitment", "processed"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let mut params = std::vec![serde_json::Value::String(signature.to_owned())];
|
|
||||||
if let std::option::Option::Some(config) = config {
|
|
||||||
params.push((*config).to_json_value());
|
|
||||||
}
|
|
||||||
return self.execute_get_transaction(role, params).await;
|
return self.execute_get_transaction(role, params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the current object-form `getTransaction` request and reports the safe identity of the endpoint that produced the successful response.
|
||||||
|
///
|
||||||
|
/// Routing, admission, timeout and retry behavior are identical to [`Self::get_transaction`]. The returned observation never contains an endpoint URL,
|
||||||
|
/// HTTP headers or a raw HTTP body.
|
||||||
|
pub async fn get_transaction_observed(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
signature: &str,
|
||||||
|
config: std::option::Option<&crate::SolanaGetTransactionConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::HttpObservedValue<std::option::Option<crate::SolanaConfirmedTransaction>>> {
|
||||||
|
let params = get_transaction_params(signature, config);
|
||||||
|
let params = match params {
|
||||||
|
std::result::Result::Ok(params) => params,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let observed = self.execute_transaction_rpc_observed("getTransaction", role, params).await;
|
||||||
|
let observed = match observed {
|
||||||
|
std::result::Result::Ok(observed) => observed,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let (value, endpoint_name, provider) = observed.into_parts();
|
||||||
|
let transaction = decode_get_transaction(value);
|
||||||
|
return match transaction {
|
||||||
|
std::result::Result::Ok(transaction) => std::result::Result::Ok(crate::HttpObservedValue::new(transaction, endpoint_name, provider)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Executes the deprecated bare-encoding `getTransaction` request form retained by Solana RPC for backwards compatibility.
|
/// Executes the deprecated bare-encoding `getTransaction` request form retained by Solana RPC for backwards compatibility.
|
||||||
#[deprecated(note = "use HttpTransportPool::get_transaction with SolanaGetTransactionConfig; the bare encoding request form is deprecated")]
|
#[deprecated(note = "use HttpTransportPool::get_transaction with SolanaGetTransactionConfig; the bare encoding request form is deprecated")]
|
||||||
pub async fn get_transaction_legacy(
|
pub async fn get_transaction_legacy(
|
||||||
@@ -1297,14 +1314,7 @@ impl crate::HttpTransportPool {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
if value.is_null() {
|
return decode_get_transaction(value);
|
||||||
return std::result::Result::Ok(std::option::Option::None);
|
|
||||||
}
|
|
||||||
let transaction = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", value);
|
|
||||||
return match transaction {
|
|
||||||
std::result::Result::Ok(transaction) => std::result::Result::Ok(std::option::Option::Some(transaction)),
|
|
||||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes typed `requestAirdrop` through the common KSP HTTP transport path.
|
/// Executes typed `requestAirdrop` through the common KSP HTTP transport path.
|
||||||
@@ -1466,6 +1476,54 @@ impl crate::HttpTransportPool {
|
|||||||
};
|
};
|
||||||
return self.execute_standard_rpc(role, method, params).await;
|
return self.execute_standard_rpc(role, method, params).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_transaction_rpc_observed(
|
||||||
|
&self,
|
||||||
|
method_name: &'static str,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
params: std::vec::Vec<serde_json::Value>,
|
||||||
|
) -> ksp_core_lib::Result<crate::HttpObservedValue<serde_json::Value>> {
|
||||||
|
let method = transaction_descriptor(method_name);
|
||||||
|
let method = match method {
|
||||||
|
std::result::Result::Ok(method) => method,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return self.execute_standard_rpc_observed(role, method, params).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_transaction_params(
|
||||||
|
signature: &str,
|
||||||
|
config: std::option::Option<&crate::SolanaGetTransactionConfig>,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
||||||
|
"getTransaction commitment must be confirmed or finalized when explicitly provided",
|
||||||
|
)
|
||||||
|
.with_context("rpc_method", "getTransaction")
|
||||||
|
.with_context("commitment", "processed"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut params = std::vec![serde_json::Value::String(signature.to_owned())];
|
||||||
|
if let std::option::Option::Some(config) = config {
|
||||||
|
params.push((*config).to_json_value());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_get_transaction(value: serde_json::Value) -> ksp_core_lib::Result<std::option::Option<crate::SolanaConfirmedTransaction>> {
|
||||||
|
if value.is_null() {
|
||||||
|
return std::result::Result::Ok(std::option::Option::None);
|
||||||
|
}
|
||||||
|
let transaction = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", value);
|
||||||
|
return match transaction {
|
||||||
|
std::result::Result::Ok(transaction) => std::result::Result::Ok(std::option::Option::Some(transaction)),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
|
|||||||
@@ -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: 48
|
// version: 49
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -199,6 +199,15 @@ fn public_pre_002_shared_rpc_types_are_constructible_from_crate_root() {
|
|||||||
assert_eq!(vote.vote_pubkey(), std::option::Option::Some(&pubkey));
|
assert_eq!(vote.vote_pubkey(), std::option::Option::Some(&pubkey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_3_6_pre_004_observed_get_transaction_surface_is_available_from_crate_root() {
|
||||||
|
let _get_transaction_observed = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_observed;
|
||||||
|
let observed: std::option::Option<
|
||||||
|
ksp_onchain_transport_lib::HttpObservedValue<std::option::Option<ksp_onchain_transport_lib::SolanaConfirmedTransaction>>,
|
||||||
|
> = std::option::Option::None;
|
||||||
|
assert!(observed.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn public_pre_003_account_wrappers_are_available_from_crate_root() {
|
fn public_pre_003_account_wrappers_are_available_from_crate_root() {
|
||||||
let _get_account_info = ksp_onchain_transport_lib::HttpTransportPool::get_account_info;
|
let _get_account_info = ksp_onchain_transport_lib::HttpTransportPool::get_account_info;
|
||||||
|
|||||||
@@ -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: 10
|
// version: 11
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
||||||
@@ -966,6 +966,80 @@ async fn typed_get_transaction_preserves_raw_json_meta_version_and_transaction_i
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_transaction_observed_reports_actual_winner_after_retry_reroute() {
|
||||||
|
let (first_url, first_handle) = serve_transaction_status_and_count("429 Too Many Requests");
|
||||||
|
let (winner_url, winner_handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.base64.json"));
|
||||||
|
let urls = [(first_url.as_str(), "first-endpoint", "first-provider"), (winner_url.as_str(), "winner-endpoint", "winner-provider")];
|
||||||
|
let mut endpoints = std::vec::Vec::with_capacity(urls.len());
|
||||||
|
for (url, endpoint_name, provider) in urls {
|
||||||
|
let role = crate::HttpEndpointRoleSettings::new(
|
||||||
|
crate::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
std::vec![crate::HttpRequestKind::wildcard()],
|
||||||
|
10,
|
||||||
|
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||||
|
);
|
||||||
|
endpoints.push(crate::HttpEndpointSettings::new(
|
||||||
|
endpoint_name,
|
||||||
|
true,
|
||||||
|
crate::HttpProviderName::new(provider),
|
||||||
|
crate::HttpClusterName::new("local"),
|
||||||
|
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||||
|
std::time::Duration::from_millis(100),
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
std::option::Option::Some(1),
|
||||||
|
std::vec![role],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let pool = crate::HttpTransportPool::new(crate::HttpTransportSettings::new(
|
||||||
|
endpoints,
|
||||||
|
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||||
|
))
|
||||||
|
.expect("observed fixture pool must build");
|
||||||
|
let config = crate::SolanaGetTransactionConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
);
|
||||||
|
let observed = pool
|
||||||
|
.get_transaction_observed(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("retry-safe observed getTransaction must succeed on the second endpoint");
|
||||||
|
assert_eq!(observed.endpoint_name(), "winner-endpoint");
|
||||||
|
assert_eq!(observed.provider().as_str(), "winner-provider");
|
||||||
|
let transaction = observed.value().as_ref().expect("winning response must contain a transaction");
|
||||||
|
assert_eq!(transaction.slot(), 431_000_062);
|
||||||
|
assert!(matches!(
|
||||||
|
transaction.transaction(),
|
||||||
|
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }
|
||||||
|
));
|
||||||
|
let (first_count, first_request) = first_handle.join().expect("first fixture server must join");
|
||||||
|
assert_eq!(first_count, 1);
|
||||||
|
assert_eq!(transaction_request_body(first_request.as_str())["method"], serde_json::json!("getTransaction"));
|
||||||
|
let winner_request = winner_handle.join().expect("winner fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(winner_request.as_str())["method"], serde_json::json!("getTransaction"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_get_transaction_observed_preserves_null_and_redacts_typed_value_debug() {
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let observed = pool
|
||||||
|
.get_transaction_observed(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("observed null getTransaction must succeed");
|
||||||
|
assert!(observed.value().is_none());
|
||||||
|
assert_eq!(observed.endpoint_name(), "fixture-0");
|
||||||
|
assert_eq!(observed.provider().as_str(), "fixture");
|
||||||
|
let rendered = format!("{observed:?}");
|
||||||
|
assert!(rendered.contains("fixture-0"));
|
||||||
|
assert!(rendered.contains("fixture"));
|
||||||
|
assert!(rendered.contains("<available>"));
|
||||||
|
assert!(!rendered.contains("fixture-signature"));
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn typed_get_transaction_preserves_unsupported_version_rpc_error() {
|
async fn typed_get_transaction_preserves_unsupported_version_rpc_error() {
|
||||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.error_unsupported_version.json"));
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.error_unsupported_version.json"));
|
||||||
|
|||||||
146
deltas/0.3.6/pre.004.md
Normal file
146
deltas/0.3.6/pre.004.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<!-- file: deltas/0.3.6/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.6-pre.004` — provenance Transport observée pour `getTransaction`
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.6-pre.003-fix.001 appliquée
|
||||||
|
workspace.package.version = 0.3.6-pre.3.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate opérateur fourni pour cette base confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS / clean
|
||||||
|
python3 scripts/audit_markdown_tables.py ... PASS / clean (264 tables / 145 fichiers)
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test -p ksp-job-api PASS / 13 unitaires + 14 canaries
|
||||||
|
cargo tree -p ksp-job-api --edges normal Core-only confirmé
|
||||||
|
cargo tree -p ksp-job-api -e features aucune feature Job API
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Matérialiser exclusivement la tranche `pre.004` du plan 027 : permettre au futur backfill de connaître le provider et l'endpoint HTTP qui ont réellement produit le succès de `getTransaction`, y compris après retry/reroutage, sans dupliquer le client RPC ni déplacer la politique Transport vers Job.
|
||||||
|
|
||||||
|
## Conception
|
||||||
|
|
||||||
|
La nouvelle enveloppe générique `HttpObservedValue<T>` contient uniquement :
|
||||||
|
|
||||||
|
- la valeur typée `T` ;
|
||||||
|
- le nom configuré et validé de l'endpoint victorieux ;
|
||||||
|
- le descripteur provider de cet endpoint.
|
||||||
|
|
||||||
|
Elle ne contient jamais :
|
||||||
|
|
||||||
|
- URL d'endpoint ;
|
||||||
|
- headers HTTP ;
|
||||||
|
- body HTTP brut ;
|
||||||
|
- credentials ou autre détail de connexion.
|
||||||
|
|
||||||
|
Son `Debug` conserve l'identité sûre de routage mais remplace systématiquement la valeur typée par `<available>` afin qu'un diagnostic générique ne rende pas accidentellement un payload transactionnel volumineux.
|
||||||
|
|
||||||
|
Le moteur HTTP commun n'est pas dupliqué. `execute_standard_rpc_with` possède toujours l'unique boucle de support, request-id, admission, deadline, retry, cooldown, HTTP, parsing JSON-RPC et accounting. Une projection de succès interne décide seulement de la forme de retour :
|
||||||
|
|
||||||
|
- `execute_standard_rpc` retourne la valeur historique sans allouer de provenance ;
|
||||||
|
- la voie interne observée capture endpoint/provider uniquement après le succès final ;
|
||||||
|
- `get_transaction_observed` réutilise cette voie et retourne `HttpObservedValue<Option<SolanaConfirmedTransaction>>`.
|
||||||
|
|
||||||
|
Les validations de paramètres `getTransaction` sont factorisées dans un helper commun. `get_transaction` et `get_transaction_observed` rejettent donc exactement les mêmes entrées et utilisent le même décodage typé. L'API historique reste source-compatible.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
Deux tests unitaires Transport :
|
||||||
|
|
||||||
|
- `typed_get_transaction_observed_reports_actual_winner_after_retry_reroute` : deux endpoints de même priorité, premier résultat HTTP `429`, retry sur le second ; la valeur observée doit rapporter `winner-endpoint` / `winner-provider`, pas le candidat initial ;
|
||||||
|
- `typed_get_transaction_observed_preserves_null_and_redacts_typed_value_debug` : `result: null` reste `None`, endpoint/provider restent disponibles et le `Debug` ne rend pas la valeur typée.
|
||||||
|
|
||||||
|
Une canarie publique :
|
||||||
|
|
||||||
|
- `public_v0_3_6_pre_004_observed_get_transaction_surface_is_available_from_crate_root` : méthode et enveloppe observée sont consommables depuis le crate-root.
|
||||||
|
|
||||||
|
Aucun fixture wire nouveau n'est nécessaire : les fixtures `get_transaction.base64.json` et `get_transaction.null.json` existantes sont réutilisées.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.3.6/pre.004.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/http_executor.rs
|
||||||
|
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/unit_tests/rpc_transactions.rs
|
||||||
|
docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md
|
||||||
|
docs/validation/023-V0_3_6_JOB_API_BACKFILL.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Mécanique Cargo :
|
||||||
|
|
||||||
|
```text
|
||||||
|
header version: 398 -> 399
|
||||||
|
workspace.package.version: 0.3.6-pre.3.fix.1 -> 0.3.6-pre.4
|
||||||
|
workspace members: inchangés
|
||||||
|
workspace dependencies: inchangées
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers supprimés
|
||||||
|
|
||||||
|
Aucun.
|
||||||
|
|
||||||
|
## Frontières conservées
|
||||||
|
|
||||||
|
- aucune crate Job ou Store n'entre dans Transport ;
|
||||||
|
- aucune dépendance ou feature n'est ajoutée ;
|
||||||
|
- aucune sélection d'endpoint, pause ou boucle de retry n'est ajoutée dans Job ;
|
||||||
|
- aucune nouvelle lecture Config/env n'est introduite ;
|
||||||
|
- aucun code, DTO, client, retry loop ou provenance kbot3 n'est copié ; la matrice fonctionnelle `pre.001` reste seulement une référence de besoin ;
|
||||||
|
- le legacy `get_transaction_legacy` reste inchangé et non observé ; le backfill v0.3.6 utilisera le formulaire moderne objet ;
|
||||||
|
- README/USAGE restent fermés jusqu'à la tranche de réconciliation documentaire prévue par le plan.
|
||||||
|
|
||||||
|
## Validations exécutées dans l'environnement d'assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
-> General Rust rule audit: clean
|
||||||
|
-> Rust export completeness audit: 0 candidate(s)
|
||||||
|
-> KSP workspace Rust rule audit: clean
|
||||||
|
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.6
|
||||||
|
-> Markdown table audit: clean
|
||||||
|
|
||||||
|
python3 -m unittest scripts/tests/test_audit_markdown_tables.py
|
||||||
|
-> 5 tests / OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier passage de l'auditeur Rust a détecté deux rustdocs manquantes sur les helpers `pub(crate)` de `HttpObservedValue`; elles ont été ajoutées avant assemblage et le second passage est intégralement propre.
|
||||||
|
|
||||||
|
L'environnement d'assemblage ne fournit ni `cargo`, ni `rustfmt`, ni `rustc`. Aucune compilation ou suite Rust de `pre.004` n'est donc annoncée comme exécutée localement.
|
||||||
|
|
||||||
|
## Gate opérateur demandé
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.6
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --edges normal
|
||||||
|
cargo tree -p ksp-onchain-transport-lib -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate doit notamment confirmer le retry/reroutage déterministe du nouveau test observé et l'absence de nouvelle dépendance/feature Transport.
|
||||||
|
|
||||||
|
## Questions ouvertes
|
||||||
|
|
||||||
|
Aucune question ne bloque `pre.005` après un gate opérateur vert de `pre.004`.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md -->
|
<!-- file: docs/plans/027-V0_3_6_JOB_API_BACKFILL_PLAN.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Plan v0.3.6 — Job API et premier backfill RAW
|
# Plan v0.3.6 — Job API et premier backfill RAW
|
||||||
|
|
||||||
@@ -406,7 +406,7 @@ Le helper de fixture est renommé `new_lifecycle()` et tous ses appels sont sync
|
|||||||
|
|
||||||
### `pre.003` — Notifications latest-value génériques
|
### `pre.003` — Notifications latest-value génériques
|
||||||
|
|
||||||
**Statut : réalisé ; corrigé par `pre.003-fix.001`, gate opérateur du fix à rejouer.**
|
**Statut : réalisé ; corrigé par `pre.003-fix.001`, gate opérateur du fix vert.**
|
||||||
|
|
||||||
Budget cible : **15-20 min**. Entrée : transitions Job stables et gate `pre.002-fix.001` vert. La tranche ajoute `JobNotificationSequence`, `JobNotification<S>`, `JobSnapshotFuture` et `JobSnapshotSource` sans nouvelle dépendance. La séquence est ordonnée, avance par `checked_add` et échoue explicitement avant tout wrap ; l'enveloppe reste immutable et son `Debug` masque le snapshot générique. Le trait source expose la valeur courante et une attente abstraite d'une valeur plus récente ; le futur public n'expose que `std::future::Future`, `Pin` et `Box`, jamais Tokio.
|
Budget cible : **15-20 min**. Entrée : transitions Job stables et gate `pre.002-fix.001` vert. La tranche ajoute `JobNotificationSequence`, `JobNotification<S>`, `JobSnapshotFuture` et `JobSnapshotSource` sans nouvelle dépendance. La séquence est ordonnée, avance par `checked_add` et échoue explicitement avant tout wrap ; l'enveloppe reste immutable et son `Debug` masque le snapshot générique. Le trait source expose la valeur courante et une attente abstraite d'une valeur plus récente ; le futur public n'expose que `std::future::Future`, `Pin` et `Box`, jamais Tokio.
|
||||||
|
|
||||||
@@ -422,9 +422,11 @@ Le gate opérateur de `pre.003` confirme `cargo fmt`, les audits Rust/Markdown e
|
|||||||
|
|
||||||
### `pre.004` — Provenance Transport observée
|
### `pre.004` — Provenance Transport observée
|
||||||
|
|
||||||
**Statut : planifié.**
|
**Statut : matérialisé ; gate opérateur à rejouer.**
|
||||||
|
|
||||||
Budget cible : **15-20 min**. Entrée : besoin de provenance confirmé par le plan. Ajouter le retour observé de `getTransaction` sans dupliquer le client. Sortie : tests de routage, retry et endpoint réellement victorieux verts.
|
Budget cible : **15-20 min**. Entrée : besoin de provenance confirmé par le plan et gate `pre.003-fix.001` vert. La tranche ajoute `HttpObservedValue<T>` comme enveloppe typée ne conservant que la valeur, le nom sûr de l'endpoint victorieux et son provider. `HttpTransportPool::get_transaction_observed` reprend exactement les validations et le moteur `execute_standard_rpc` existants ; le moteur commun possède désormais une voie interne observée et l'API historique continue à ne retourner que la valeur.
|
||||||
|
|
||||||
|
Les tests couvrent le succès direct `null`, la surface publique et surtout un retry `429` entre deux endpoints de même priorité : le résultat observé rapporte le second endpoint/provider qui a réellement produit la réponse, jamais le candidat initial. URL, headers et body HTTP brut restent absents du contrat, et le `Debug` de l'enveloppe ne rend pas la valeur typée. Sortie attendue après gate : routage/retry/provenance Transport verts sans nouvelle dépendance ni changement de politique.
|
||||||
|
|
||||||
### `pre.005` — Fondation Backfill et découverte
|
### `pre.005` — Fondation Backfill et découverte
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/023-V0_3_6_JOB_API_BACKFILL.md -->
|
<!-- file: docs/validation/023-V0_3_6_JOB_API_BACKFILL.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Validation v0.3.6 — Job API et premier backfill RAW
|
# Validation v0.3.6 — Job API et premier backfill RAW
|
||||||
|
|
||||||
@@ -54,6 +54,8 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
|
|||||||
- [X] Gate opérateur de `pre.003` : `cargo fmt`, audits Rust/Markdown et `cargo check --workspace` verts ; Clippy `--all-targets` et `cargo test -p ksp-job-api` échouent uniquement sur `E0423` dans le helper `exhausted_notification_sequence()`.
|
- [X] Gate opérateur de `pre.003` : `cargo fmt`, audits Rust/Markdown et `cargo check --workspace` verts ; Clippy `--all-targets` et `cargo test -p ksp-job-api` échouent uniquement sur `E0423` dans le helper `exhausted_notification_sequence()`.
|
||||||
- [X] Les arbres Cargo normal/features de ce gate confirment que `ksp-job-api` dépend toujours uniquement de `ksp-core-lib` et n'ouvre aucune feature.
|
- [X] Les arbres Cargo normal/features de ce gate confirment que `ksp-job-api` dépend toujours uniquement de `ksp-core-lib` et n'ouvre aucune feature.
|
||||||
- [X] `pre.003-fix.001` remplace uniquement la construction via le re-export racine par le constructeur privé directement visible dans le module `notification`, sans changer l'API publique ni le nombre de tests.
|
- [X] `pre.003-fix.001` remplace uniquement la construction via le re-export racine par le constructeur privé directement visible dans le module `notification`, sans changer l'API publique ni le nombre de tests.
|
||||||
|
- [X] Gate opérateur de `pre.003-fix.001` : `cargo fmt`, audits Rust/Markdown, `cargo check`, Clippy, treize unitaires + quatorze canaries `ksp-job-api` et arbres Cargo normal/features verts.
|
||||||
|
- [X] `pre.004` matérialise une voie observée additive de `getTransaction` sans modifier le client, les settings, le routage ni la politique de retry Transport.
|
||||||
- [X] Le workspace compte 14 crates et aucune crate Job au point de départ.
|
- [X] Le workspace compte 14 crates et aucune crate Job au point de départ.
|
||||||
|
|
||||||
## 4. Autorités et cohérence documentaire
|
## 4. Autorités et cohérence documentaire
|
||||||
@@ -121,9 +123,9 @@ Aucune entrée absolue, traversée, avec séparateur inversé ou lien symbolique
|
|||||||
## 9. Acquisition Transport et retry
|
## 9. Acquisition Transport et retry
|
||||||
|
|
||||||
- [ ] Les wrappers typés officiels restent l'unique chemin RPC.
|
- [ ] Les wrappers typés officiels restent l'unique chemin RPC.
|
||||||
- [ ] Retour observé additif de `getTransaction` couvert.
|
- [X] Retour observé additif de `getTransaction` matérialisé ; gate opérateur à rejouer.
|
||||||
- [ ] Fournisseur et endpoint rapportés correspondent au succès réel après reroutage.
|
- [X] Canarie déterministe : un `429` sur le premier endpoint reroute et rapporte le second provider/endpoint victorieux ; gate opérateur à rejouer.
|
||||||
- [ ] URL, headers et body brut restent privés.
|
- [X] `HttpObservedValue<T>` ne contient que valeur typée, endpoint sûr et provider ; URL, headers et body HTTP brut restent privés.
|
||||||
- [ ] Aucun retry, pacing ou sélection d'endpoint dans Job.
|
- [ ] Aucun retry, pacing ou sélection d'endpoint dans Job.
|
||||||
- [ ] Aucune détection d'erreur par chaîne.
|
- [ ] Aucune détection d'erreur par chaîne.
|
||||||
- [ ] Une erreur Transport finale arrête les admissions avec un code Job sûr.
|
- [ ] Une erreur Transport finale arrête les admissions avec un code Job sûr.
|
||||||
|
|||||||
Reference in New Issue
Block a user