v0.2.3-pre.006
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 125
|
# version: 126
|
||||||
|
|
||||||
[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.5"
|
version = "0.2.3-pre.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: supplied blockhash is not valid"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"airdrop-fixture-signature-111111111111111111111111111111111111111111111111","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Error processing Instruction 0","data":{"err":{"InstructionError":[0,"Custom"]},"logs":["Program log: fixture preflight failure"],"unitsConsumed":123}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"send-fixture-signature-22222222222222222222222222222222222222222222222222","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: 14
|
// version: 15
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -10,10 +10,9 @@
|
|||||||
//! 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 seven read wrappers through `pre.004`,
|
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives, all eight read wrappers and both write
|
||||||
//! including bounded prioritization-fee, address-signature and signature-status queries. `getTransaction`, both write submissions and
|
//! submissions through `pre.006`, including complete modern/legacy `getTransaction` coverage and centralized no-resend protection for writes.
|
||||||
//! `simulateTransaction`, plus the
|
//! `simulateTransaction` and the `0.2.4` family remain staged.
|
||||||
//! `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: 5
|
// version: 6
|
||||||
|
|
||||||
/// 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)]
|
||||||
@@ -321,14 +321,12 @@ impl SolanaRequestAirdropConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the airdrop config would serialize to an empty object.
|
/// Returns whether the airdrop 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.recent_blockhash.is_none() && self.commitment.is_none();
|
return self.recent_blockhash.is_none() && self.commitment.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(blockhash) = self.recent_blockhash.as_ref() {
|
if let std::option::Option::Some(blockhash) = self.recent_blockhash.as_ref() {
|
||||||
@@ -395,7 +393,6 @@ impl SolanaSendTransactionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the send config would serialize to an empty object.
|
/// Returns whether the send 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.skip_preflight.is_none()
|
return self.skip_preflight.is_none()
|
||||||
&& self.preflight_commitment.is_none()
|
&& self.preflight_commitment.is_none()
|
||||||
@@ -406,7 +403,6 @@ impl SolanaSendTransactionConfig {
|
|||||||
|
|
||||||
/// 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.skip_preflight {
|
if let std::option::Option::Some(value) = self.skip_preflight {
|
||||||
@@ -1314,6 +1310,53 @@ impl crate::HttpTransportPool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes typed `requestAirdrop` through the common KSP HTTP transport path.
|
||||||
|
///
|
||||||
|
/// The RPC creates and submits a faucet transaction, so its audited descriptor is `WriteSubmission / NeverAfterDispatch`. The optional
|
||||||
|
/// `recentBlockhash` field is retained from the targeted Agave runtime even though the public Solana page currently documents only `commitment`.
|
||||||
|
pub async fn request_airdrop(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
recipient: &ksp_core_lib::Pubkey,
|
||||||
|
lamports: u64,
|
||||||
|
config: std::option::Option<&crate::SolanaRequestAirdropConfig>,
|
||||||
|
) -> ksp_core_lib::Result<std::string::String> {
|
||||||
|
let mut params = std::vec![serde_json::Value::String(recipient.to_string()), serde_json::json!(lamports)];
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& !config.is_empty()
|
||||||
|
{
|
||||||
|
params.push(config.to_json_value());
|
||||||
|
}
|
||||||
|
let value = self.execute_transaction_rpc("requestAirdrop", role, params).await;
|
||||||
|
return match value {
|
||||||
|
std::result::Result::Ok(value) => crate::decode_wire_json::<std::string::String>("requestAirdrop", value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes typed `sendTransaction` through the common KSP HTTP transport path.
|
||||||
|
///
|
||||||
|
/// Transport forwards an already serialized and signed transaction without decoding or modifying it. `config.maxRetries` controls node-side
|
||||||
|
/// retransmission only; KSP's HTTP retry policy remains governed by the central `WriteSubmission / NeverAfterDispatch` descriptor.
|
||||||
|
pub async fn send_transaction(
|
||||||
|
&self,
|
||||||
|
role: &crate::HttpRoleName,
|
||||||
|
transaction: &str,
|
||||||
|
config: std::option::Option<&crate::SolanaSendTransactionConfig>,
|
||||||
|
) -> ksp_core_lib::Result<std::string::String> {
|
||||||
|
let mut params = std::vec![serde_json::Value::String(transaction.to_owned())];
|
||||||
|
if let std::option::Option::Some(config) = config
|
||||||
|
&& !config.is_empty()
|
||||||
|
{
|
||||||
|
params.push((*config).to_json_value());
|
||||||
|
}
|
||||||
|
let value = self.execute_transaction_rpc("sendTransaction", role, params).await;
|
||||||
|
return match value {
|
||||||
|
std::result::Result::Ok(value) => crate::decode_wire_json::<std::string::String>("sendTransaction", value),
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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: 13
|
// version: 14
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -300,3 +300,24 @@ fn public_transaction_pre_005_get_transaction_complete_request_forms_are_availab
|
|||||||
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed));
|
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed));
|
||||||
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(0));
|
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_transaction_pre_006_write_wrappers_and_complete_configs_are_available_from_crate_root() {
|
||||||
|
let _request_airdrop = ksp_onchain_transport_lib::HttpTransportPool::request_airdrop;
|
||||||
|
let _send_transaction = ksp_onchain_transport_lib::HttpTransportPool::send_transaction;
|
||||||
|
let airdrop = ksp_onchain_transport_lib::SolanaRequestAirdropConfig::new(
|
||||||
|
std::option::Option::Some("recent-blockhash".to_owned()),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
);
|
||||||
|
assert_eq!(airdrop.recent_blockhash(), std::option::Option::Some("recent-blockhash"));
|
||||||
|
let send = ksp_onchain_transport_lib::SolanaSendTransactionConfig::new(
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::Some(5),
|
||||||
|
std::option::Option::Some(431_000_000),
|
||||||
|
);
|
||||||
|
assert_eq!(send.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
|
||||||
|
assert_eq!(send.max_retries(), std::option::Option::Some(5));
|
||||||
|
assert_eq!(send.min_context_slot(), std::option::Option::Some(431_000_000));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: 10
|
// version: 11
|
||||||
|
|
||||||
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
|
||||||
|
|
||||||
@@ -326,3 +326,36 @@ fn release_pre_005_transaction_read_subset_adds_complete_get_transaction_without
|
|||||||
assert_eq!(actual.len(), 8);
|
assert_eq!(actual.len(), 8);
|
||||||
assert_eq!(deferred.len(), 3);
|
assert_eq!(deferred.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_pre_006_transaction_subset_adds_both_write_submissions_without_advancing_simulation() {
|
||||||
|
let reads = std::vec![
|
||||||
|
"getFeeForMessage",
|
||||||
|
"getLatestBlockhash",
|
||||||
|
"getRecentPrioritizationFees",
|
||||||
|
"getSignaturesForAddress",
|
||||||
|
"getSignatureStatuses",
|
||||||
|
"getTransaction",
|
||||||
|
"getTransactionCount",
|
||||||
|
"isBlockhashValid",
|
||||||
|
];
|
||||||
|
let writes = std::vec!["requestAirdrop", "sendTransaction"];
|
||||||
|
for method_name in &reads {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 read descriptor must exist");
|
||||||
|
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 &writes {
|
||||||
|
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.006 write descriptor must exist");
|
||||||
|
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::WriteSubmission);
|
||||||
|
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
|
||||||
|
}
|
||||||
|
let simulation = ksp_onchain_transport_lib::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must remain registered");
|
||||||
|
assert_eq!(simulation.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
|
||||||
|
assert_eq!(reads.len(), 8);
|
||||||
|
assert_eq!(writes.len(), 2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: 5
|
// version: 6
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
||||||
@@ -290,27 +290,34 @@ fn simulation_result_distinguishes_omitted_from_explicit_null_fields() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn transaction_pool_for_url(url: &str) -> crate::HttpTransportPool {
|
fn transaction_pool_for_url(url: &str) -> crate::HttpTransportPool {
|
||||||
let role = crate::HttpEndpointRoleSettings::new(
|
return transaction_pool_for_urls(&[(url, 10)], std::time::Duration::from_secs(1), 0);
|
||||||
crate::HttpRoleName::new("default"),
|
}
|
||||||
true,
|
|
||||||
std::vec![crate::HttpRequestKind::wildcard()],
|
fn transaction_pool_for_urls(urls: &[(&str, u32)], request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
|
||||||
10,
|
let mut endpoints = std::vec::Vec::with_capacity(urls.len());
|
||||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
for (index, (url, priority)) in urls.iter().enumerate() {
|
||||||
);
|
let role = crate::HttpEndpointRoleSettings::new(
|
||||||
let endpoint = crate::HttpEndpointSettings::new(
|
crate::HttpRoleName::new("default"),
|
||||||
"fixture",
|
true,
|
||||||
true,
|
std::vec![crate::HttpRequestKind::wildcard()],
|
||||||
crate::HttpProviderName::new("fixture"),
|
*priority,
|
||||||
crate::HttpClusterName::new("local"),
|
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||||
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
);
|
||||||
std::time::Duration::from_secs(1),
|
endpoints.push(crate::HttpEndpointSettings::new(
|
||||||
std::time::Duration::from_secs(1),
|
format!("fixture-{index}"),
|
||||||
std::option::Option::Some(1),
|
true,
|
||||||
std::vec![role],
|
crate::HttpProviderName::new("fixture"),
|
||||||
);
|
crate::HttpClusterName::new("local"),
|
||||||
|
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||||
|
std::time::Duration::from_millis(100),
|
||||||
|
request_timeout,
|
||||||
|
std::option::Option::Some(1),
|
||||||
|
std::vec![role],
|
||||||
|
));
|
||||||
|
}
|
||||||
let settings = crate::HttpTransportSettings::new(
|
let settings = crate::HttpTransportSettings::new(
|
||||||
std::vec![endpoint],
|
endpoints,
|
||||||
crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
|
crate::HttpRetrySettings::new(max_retries, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||||
);
|
);
|
||||||
return crate::HttpTransportPool::new(settings).expect("fixture pool must build");
|
return crate::HttpTransportPool::new(settings).expect("fixture pool must build");
|
||||||
}
|
}
|
||||||
@@ -328,6 +335,69 @@ fn serve_transaction_once(body: &'static str) -> (std::string::String, std::thre
|
|||||||
return (format!("http://{address}"), handle);
|
return (format!("http://{address}"), handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serve_transaction_status_and_count(status_line: &'static str) -> (std::string::String, std::thread::JoinHandle<(usize, std::string::String)>) {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||||
|
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().expect("fixture server must accept first request");
|
||||||
|
let first_request = read_transaction_request(&mut stream);
|
||||||
|
let response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||||
|
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||||
|
let mut count = 1_usize;
|
||||||
|
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(120);
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
match listener.accept() {
|
||||||
|
std::result::Result::Ok((mut retry_stream, _)) => {
|
||||||
|
let _ = read_transaction_request(&mut retry_stream);
|
||||||
|
std::io::Write::write_all(&mut retry_stream, response.as_bytes()).expect("fixture retry response must write");
|
||||||
|
count = count.saturating_add(1);
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => panic!("fixture listener failed while counting retries: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (count, first_request);
|
||||||
|
});
|
||||||
|
return (format!("http://{address}"), handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serve_transaction_timeout_and_count(delay: std::time::Duration) -> (std::string::String, std::thread::JoinHandle<(usize, std::string::String)>) {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||||
|
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let (mut stream, _) = listener.accept().expect("fixture server must accept first request");
|
||||||
|
let first_request = read_transaction_request(&mut stream);
|
||||||
|
std::thread::sleep(delay);
|
||||||
|
let mut count = 1_usize;
|
||||||
|
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
|
||||||
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(120);
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
match listener.accept() {
|
||||||
|
std::result::Result::Ok((mut retry_stream, _)) => {
|
||||||
|
let _ = read_transaction_request(&mut retry_stream);
|
||||||
|
count = count.saturating_add(1);
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => panic!("fixture listener failed while counting timeout retries: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (count, first_request);
|
||||||
|
});
|
||||||
|
return (format!("http://{address}"), handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unused_local_url() -> std::string::String {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("unused-port probe must bind");
|
||||||
|
let address = listener.local_addr().expect("unused-port probe address must resolve");
|
||||||
|
drop(listener);
|
||||||
|
return format!("http://{address}");
|
||||||
|
}
|
||||||
|
|
||||||
fn read_transaction_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
fn read_transaction_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||||
let mut bytes = std::vec::Vec::new();
|
let mut bytes = std::vec::Vec::new();
|
||||||
let mut buffer = [0_u8; 1024];
|
let mut buffer = [0_u8; 1024];
|
||||||
@@ -874,3 +944,198 @@ async fn typed_get_transaction_rejects_processed_commitment_before_io() {
|
|||||||
assert_eq!(error.context()[1].key(), "commitment");
|
assert_eq!(error.context()[1].key(), "commitment");
|
||||||
assert_eq!(error.context()[1].value(), "processed");
|
assert_eq!(error.context()[1].value(), "processed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_request_airdrop_exposes_runtime_config_and_canonicalizes_empty_config() {
|
||||||
|
let recipient = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let config = crate::SolanaRequestAirdropConfig::new(
|
||||||
|
std::option::Option::Some("recent-blockhash-fixture".to_owned()),
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Finalized),
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let signature = pool
|
||||||
|
.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1_000_000_000, std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("requestAirdrop full-config fixture must succeed");
|
||||||
|
assert_eq!(signature, "airdrop-fixture-signature-111111111111111111111111111111111111111111111111");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("requestAirdrop"));
|
||||||
|
assert_eq!(
|
||||||
|
body["params"],
|
||||||
|
serde_json::json!([
|
||||||
|
"11111111111111111111111111111111",
|
||||||
|
1000000000,
|
||||||
|
{"recentBlockhash":"recent-blockhash-fixture","commitment":"finalized"}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let empty = crate::SolanaRequestAirdropConfig::default();
|
||||||
|
let _ = pool
|
||||||
|
.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::Some(&empty))
|
||||||
|
.await
|
||||||
|
.expect("requestAirdrop empty-config fixture must succeed");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["11111111111111111111111111111111", 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_send_transaction_exposes_all_current_options_and_both_binary_encodings() {
|
||||||
|
let config = crate::SolanaSendTransactionConfig::new(
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
|
||||||
|
std::option::Option::Some(7),
|
||||||
|
std::option::Option::Some(431_000_000),
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let signature = pool
|
||||||
|
.send_transaction(&crate::HttpRoleName::new("default"), "opaque-base64-transaction", std::option::Option::Some(&config))
|
||||||
|
.await
|
||||||
|
.expect("sendTransaction full-config fixture must succeed");
|
||||||
|
assert_eq!(signature, "send-fixture-signature-22222222222222222222222222222222222222222222222222");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
let body = transaction_request_body(request.as_str());
|
||||||
|
assert_eq!(body["method"], serde_json::json!("sendTransaction"));
|
||||||
|
assert_eq!(
|
||||||
|
body["params"],
|
||||||
|
serde_json::json!([
|
||||||
|
"opaque-base64-transaction",
|
||||||
|
{
|
||||||
|
"skipPreflight":true,
|
||||||
|
"preflightCommitment":"confirmed",
|
||||||
|
"encoding":"base64",
|
||||||
|
"maxRetries":7,
|
||||||
|
"minContextSlot":431000000
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
let base58 = crate::SolanaSendTransactionConfig::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base58),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let _ = pool
|
||||||
|
.send_transaction(&crate::HttpRoleName::new("default"), "opaque-base58-transaction", std::option::Option::Some(&base58))
|
||||||
|
.await
|
||||||
|
.expect("sendTransaction base58 fixture must succeed");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
|
||||||
|
|
||||||
|
let empty = crate::SolanaSendTransactionConfig::default();
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
|
||||||
|
let pool = transaction_pool_for_url(url.as_str());
|
||||||
|
let _ = pool
|
||||||
|
.send_transaction(&crate::HttpRoleName::new("default"), "opaque-default-transaction", std::option::Option::Some(&empty))
|
||||||
|
.await
|
||||||
|
.expect("sendTransaction empty config must canonicalize to omission");
|
||||||
|
let request = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-default-transaction"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_write_wrappers_preserve_rpc_application_errors_without_transport_retry() {
|
||||||
|
let recipient = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.error.json"));
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||||
|
let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("requestAirdrop RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
|
||||||
|
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.preflight_error.json"));
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||||
|
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("sendTransaction preflight error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||||
|
handle.join().expect("fixture server must join");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_write_wrappers_never_resend_after_http_429_dispatch() {
|
||||||
|
let recipient = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let (url, handle) = serve_transaction_status_and_count("429 Too Many Requests");
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||||
|
let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("airdrop 429 must stop after dispatch").code(), crate::ERROR_CODE_RATE_LIMITED);
|
||||||
|
let (count, request) = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
|
||||||
|
|
||||||
|
let (url, handle) = serve_transaction_status_and_count("429 Too Many Requests");
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||||
|
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("send 429 must stop after dispatch").code(), crate::ERROR_CODE_RATE_LIMITED);
|
||||||
|
let (count, request) = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("sendTransaction"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_write_wrappers_never_resend_after_temporary_http_dispatch() {
|
||||||
|
let recipient = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let (url, handle) = serve_transaction_status_and_count("503 Service Unavailable");
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||||
|
let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("airdrop 503 must stop after dispatch").code(), crate::ERROR_CODE_HTTP_REQUEST_FAILED);
|
||||||
|
let (count, _) = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
|
||||||
|
let (url, handle) = serve_transaction_status_and_count("503 Service Unavailable");
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||||
|
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("send 503 must stop after dispatch").code(), crate::ERROR_CODE_HTTP_REQUEST_FAILED);
|
||||||
|
let (count, _) = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_write_wrappers_never_resend_after_ambiguous_timeout_dispatch() {
|
||||||
|
let recipient = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let (url, handle) = serve_transaction_timeout_and_count(std::time::Duration::from_millis(80));
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(20), 3);
|
||||||
|
let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("airdrop timeout must stop after ambiguous dispatch").code(), crate::ERROR_CODE_TIMEOUT);
|
||||||
|
let (count, _) = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
|
||||||
|
let (url, handle) = serve_transaction_timeout_and_count(std::time::Duration::from_millis(80));
|
||||||
|
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(20), 3);
|
||||||
|
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||||
|
assert_eq!(result.expect_err("send timeout must stop after ambiguous dispatch").code(), crate::ERROR_CODE_TIMEOUT);
|
||||||
|
let (count, _) = handle.join().expect("fixture server must join");
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn typed_write_wrappers_can_retry_when_connection_failure_proves_not_dispatched() {
|
||||||
|
let recipient = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let closed = unused_local_url();
|
||||||
|
let (secondary, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json"));
|
||||||
|
let pool = transaction_pool_for_urls(&[(closed.as_str(), 10), (secondary.as_str(), 10)], std::time::Duration::from_millis(500), 1);
|
||||||
|
let result = pool
|
||||||
|
.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("NotDispatched connection failure may retry requestAirdrop safely");
|
||||||
|
assert_eq!(result, "airdrop-fixture-signature-111111111111111111111111111111111111111111111111");
|
||||||
|
let request = handle.join().expect("secondary fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
|
||||||
|
|
||||||
|
let closed = unused_local_url();
|
||||||
|
let (secondary, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
|
||||||
|
let pool = transaction_pool_for_urls(&[(closed.as_str(), 10), (secondary.as_str(), 10)], std::time::Duration::from_millis(500), 1);
|
||||||
|
let result = pool
|
||||||
|
.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("NotDispatched connection failure may retry sendTransaction safely");
|
||||||
|
assert_eq!(result, "send-fixture-signature-22222222222222222222222222222222222222222222222222");
|
||||||
|
let request = handle.join().expect("secondary fixture server must join");
|
||||||
|
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("sendTransaction"));
|
||||||
|
}
|
||||||
|
|||||||
282
deltas/0.2.3/pre.006.md
Normal file
282
deltas/0.2.3/pre.006.md
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
<!-- file: deltas/0.2.3/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.3-pre.006` — `requestAirdrop` + `sendTransaction` et preuve no-resend
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
Livraison précédente validée localement par l'opérateur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.005
|
||||||
|
workspace.package.version = "0.2.3-pre.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation opérateur du 2026-08-18 a confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats Transport de cette base :
|
||||||
|
|
||||||
|
```text
|
||||||
|
168 unit tests
|
||||||
|
17 public API tests
|
||||||
|
11 release completeness tests
|
||||||
|
0 warning signalé par check/clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Activer les deux écritures de la famille Transactions :
|
||||||
|
|
||||||
|
```text
|
||||||
|
requestAirdrop
|
||||||
|
sendTransaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux wrappers restent attachés aux descriptors centraux déjà audités :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RpcOperationKind::WriteSubmission
|
||||||
|
TransportRetryClass::NeverAfterDispatch
|
||||||
|
```
|
||||||
|
|
||||||
|
Après cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
10 / 11 wrappers Transactions exécutés
|
||||||
|
8 Read
|
||||||
|
2 WriteSubmission
|
||||||
|
1 Simulation encore différée
|
||||||
|
```
|
||||||
|
|
||||||
|
`simulateTransaction` reste réservée à `pre.007`.
|
||||||
|
|
||||||
|
## Version Cargo
|
||||||
|
|
||||||
|
Conformément à `VER-ID-009` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.3-pre.5 -> 0.2.3-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance ni feature Cargo n'est ajoutée.
|
||||||
|
|
||||||
|
## Complétude `requestAirdrop`
|
||||||
|
|
||||||
|
API publique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HttpTransportPool::request_airdrop(
|
||||||
|
role,
|
||||||
|
recipient: &Pubkey,
|
||||||
|
lamports: u64,
|
||||||
|
Option<&SolanaRequestAirdropConfig>,
|
||||||
|
)
|
||||||
|
-> Result<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
La page publique Solana courante documente `commitment` dans l'objet de config. La source primaire Agave `v4.2.1` conserve en plus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
recentBlockhash: Option<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
KSP l'expose donc conformément à `KSP-TRANSPORT-007` au lieu de réduire la surface au seul exemple/document public visible.
|
||||||
|
|
||||||
|
Le résultat reste la signature de transaction retournée par le faucet sous forme de chaîne wire. Transport ne prend pas une dépendance Signature uniquement
|
||||||
|
pour redécoder une valeur que le provider vient de produire.
|
||||||
|
|
||||||
|
Une config vide est canonicalisée vers l'omission du troisième paramètre, syntaxe strictement équivalente autorisée par `KSP-TRANSPORT-007`.
|
||||||
|
|
||||||
|
## Complétude `sendTransaction`
|
||||||
|
|
||||||
|
API publique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HttpTransportPool::send_transaction(
|
||||||
|
role,
|
||||||
|
transaction: &str,
|
||||||
|
Option<&SolanaSendTransactionConfig>,
|
||||||
|
)
|
||||||
|
-> Result<String>
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport reçoit une transaction déjà construite, signée et encodée. Il ne la décode, ne la signe et ne la modifie pas.
|
||||||
|
|
||||||
|
La config expose la surface courante complète :
|
||||||
|
|
||||||
|
```text
|
||||||
|
encoding Base58 | Base64
|
||||||
|
skipPreflight Option<bool>
|
||||||
|
preflightCommitment Option<SolanaCommitment>
|
||||||
|
maxRetries Option<usize>
|
||||||
|
minContextSlot Option<u64>
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux encodings binaires sont exercés par fixtures HTTP locales.
|
||||||
|
|
||||||
|
Une config vide est canonicalisée vers l'omission du second paramètre.
|
||||||
|
|
||||||
|
### Distinction impérative des retries
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaSendTransactionConfig.maxRetries = retransmissions node-side après acceptation RPC
|
||||||
|
HttpRetrySettings = retries HTTP Transport KSP
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier champ n'accorde aucune permission de resoumission HTTP au wrapper.
|
||||||
|
|
||||||
|
## Preuves end-to-end de no-resend
|
||||||
|
|
||||||
|
Les tests ne se limitent pas à `evaluate_transport_retry`.
|
||||||
|
|
||||||
|
Ils exercent réellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
wrapper typed
|
||||||
|
-> descriptor central
|
||||||
|
-> execute_standard_rpc
|
||||||
|
-> client HTTP fixture
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour **chacun** de `requestAirdrop` et `sendTransaction`, un serveur local compte les requêtes reçues.
|
||||||
|
|
||||||
|
Cas ambigus après dispatch :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP 429 -> erreur rate_limited -> compteur = 1
|
||||||
|
HTTP 503 -> erreur http_request_failed -> compteur = 1
|
||||||
|
timeout -> erreur timeout -> compteur = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Même avec plusieurs retries configurés dans `HttpRetrySettings`, aucune seconde soumission n'est émise après ces trois états ambigus.
|
||||||
|
|
||||||
|
Cas explicitement sûr :
|
||||||
|
|
||||||
|
```text
|
||||||
|
connexion refusée -> ERROR_CODE_HTTP_CONNECTION_FAILED -> NotDispatched
|
||||||
|
```
|
||||||
|
|
||||||
|
Avec deux endpoints de même priorité, la première tentative échoue avant dispatch et le retry central peut atteindre le second endpoint. Cette preuve est
|
||||||
|
exercée séparément pour les deux wrappers.
|
||||||
|
|
||||||
|
## Erreurs RPC applicatives
|
||||||
|
|
||||||
|
Les fixtures couvrent aussi :
|
||||||
|
|
||||||
|
- une erreur `requestAirdrop` de paramètres/blockhash ;
|
||||||
|
- une erreur `sendTransaction` de preflight/simulation.
|
||||||
|
|
||||||
|
Ces réponses restent `ERROR_CODE_RPC_APPLICATION_ERROR` et ne deviennent jamais des retries transport.
|
||||||
|
|
||||||
|
## `KSP-TRANSPORT-007` rétroactif
|
||||||
|
|
||||||
|
La question opérateur sur les wrappers antérieurs est enregistrée dans le plan `010`.
|
||||||
|
|
||||||
|
Les audits `0.2.1`/`0.2.2` avaient déjà recherché la surface complète. Un contrôle ciblé pendant cette tranche confirme notamment l'alignement KSP avec
|
||||||
|
Agave `v4.2.1` pour :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RpcAccountInfoConfig
|
||||||
|
RpcProgramAccountsConfig
|
||||||
|
RpcLargestAccountsConfig
|
||||||
|
RpcLeaderScheduleConfig
|
||||||
|
RpcGetVoteAccountsConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
Mais la règle n'était pas encore un critère de clôture nommé. La prévision devient donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.007 simulateTransaction complet
|
||||||
|
pre.008 audit rétroactif KSP-TRANSPORT-007 sur 0.2.1 -> 0.2.3 + corrections éventuelles
|
||||||
|
pre.009 clôture finale documentaire/validation/smoke/prompt 0.2.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche supplémentaire évite de transformer la clôture en remédiation fonctionnelle tardive.
|
||||||
|
|
||||||
|
## Documentation/source synchronisée
|
||||||
|
|
||||||
|
`crates/ksp-onchain-transport-lib/src/lib.rs` est corrigé pour refléter la surface réellement disponible après `pre.006` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
8 reads Transactions
|
||||||
|
2 write submissions
|
||||||
|
simulateTransaction encore staged
|
||||||
|
```
|
||||||
|
|
||||||
|
Le plan `docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md` passe en version 5 et enregistre le nouveau découpage jusqu'à `pre.009`.
|
||||||
|
|
||||||
|
`CHANGELOG.md` reste réservé à `0.2.3-rel.001`.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
Sept tests unitaires couvrent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
requestAirdrop config complète + canonicalisation config vide
|
||||||
|
sendTransaction config complète + base58/base64 + config vide
|
||||||
|
erreurs RPC applicatives des deux writes
|
||||||
|
no-resend après HTTP 429 pour les deux writes
|
||||||
|
no-resend après HTTP 503 pour les deux writes
|
||||||
|
no-resend après timeout ambigu pour les deux writes
|
||||||
|
retry/fallback autorisé après connexion NotDispatched pour les deux writes
|
||||||
|
```
|
||||||
|
|
||||||
|
Un test public vérifie la disponibilité des deux wrappers/configs à la racine de crate.
|
||||||
|
|
||||||
|
Une canarie release vérifie le sous-ensemble `pre.006` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
8 Read / RetrySafe
|
||||||
|
2 WriteSubmission / NeverAfterDispatch
|
||||||
|
simulateTransaction encore différée
|
||||||
|
```
|
||||||
|
|
||||||
|
Cible après application :
|
||||||
|
|
||||||
|
```text
|
||||||
|
175 unit tests
|
||||||
|
18 public API tests
|
||||||
|
12 release completeness tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers du delta
|
||||||
|
|
||||||
|
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
|
||||||
|
docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajoutés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/request_airdrop.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/request_airdrop.error.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/send_transaction.success.json
|
||||||
|
crates/ksp-onchain-transport-lib/fixtures/http/send_transaction.preflight_error.json
|
||||||
|
deltas/0.2.3/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validations attendues opérateur
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
L'environnement de préparation du delta ne possède pas Cargo/Rustfmt. Aucune de ces commandes n'est donc déclarée réussie avant preuve opérateur.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md -->
|
<!-- file: docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Plan `0.2.3` — HTTP Transactions
|
# Plan `0.2.3` — HTTP Transactions
|
||||||
|
|
||||||
@@ -589,6 +589,27 @@ Un candidat raisonnable pour la clôture est un smoke Transport pur combinant `g
|
|||||||
simulation live n'est retenue que si une fixture transactionnelle stable peut être fournie sans déplacer la construction/signature métier dans
|
simulation live n'est retenue que si une fixture transactionnelle stable peut être fournie sans déplacer la construction/signature métier dans
|
||||||
Transport.
|
Transport.
|
||||||
|
|
||||||
|
## Audit rétroactif `KSP-TRANSPORT-007` planifié
|
||||||
|
|
||||||
|
La règle `KSP-TRANSPORT-007` formalise en `pre.005` un principe déjà appliqué dans les audits de `0.2.1` et `0.2.2`, mais elle n'était pas encore un
|
||||||
|
critère de clôture nommé lors de ces releases. Un contrôle rétroactif dédié est donc ajouté avant la clôture documentaire.
|
||||||
|
|
||||||
|
L'audit `pre.008` couvrira au minimum :
|
||||||
|
|
||||||
|
```text
|
||||||
|
4 wrappers foundation de 0.2.1
|
||||||
|
22 wrappers Accounts/Tokens/Cluster de 0.2.2
|
||||||
|
11 wrappers Transactions de 0.2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Il comparera la surface publique KSP aux paramètres/configs/overloads et formes de réponse des sources normatives courantes retenues. Les syntaxes
|
||||||
|
strictement équivalentes peuvent rester canonicalisées conformément à la règle. Toute possibilité supportée manquante sera corrigée dans cette tranche ;
|
||||||
|
si aucun manque n'est trouvé, la tranche fournira la preuve/canarie d'audit sans modification fonctionnelle artificielle.
|
||||||
|
|
||||||
|
Un contrôle ciblé effectué pendant `pre.006` confirme déjà que les configs structurantes de `0.2.2` (`RpcAccountInfoConfig`,
|
||||||
|
`RpcProgramAccountsConfig`, `RpcLargestAccountsConfig`, `RpcLeaderScheduleConfig`, `RpcGetVoteAccountsConfig`) correspondent aux champs Agave
|
||||||
|
`v4.2.1` exposés par les DTOs KSP. Ce contrôle ne remplace pas l'audit exhaustif `pre.008`.
|
||||||
|
|
||||||
## Prévision souple des prereleases
|
## Prévision souple des prereleases
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -599,11 +620,14 @@ pre.004 getRecentPrioritizationFees + getSignaturesForAddress + getSignatureSta
|
|||||||
pre.005 getTransaction moderne + compatibilité legacy + transaction/meta/version wire
|
pre.005 getTransaction moderne + compatibilité legacy + transaction/meta/version wire
|
||||||
pre.006 requestAirdrop + sendTransaction + preuves end-to-end no-resend
|
pre.006 requestAirdrop + sendTransaction + preuves end-to-end no-resend
|
||||||
pre.007 simulateTransaction + résultat riche + invariants de config
|
pre.007 simulateTransaction + résultat riche + invariants de config
|
||||||
pre.008 réaudit final + canaries + smoke opt-in si pertinent + README/USAGE + validation + prompt 0.2.4
|
pre.008 audit rétroactif KSP-TRANSPORT-007 sur 0.2.1 -> 0.2.3 + remédiations éventuelles
|
||||||
|
pre.009 réaudit final + canaries + smoke opt-in si pertinent + README/USAGE + validation + prompt 0.2.4
|
||||||
```
|
```
|
||||||
|
|
||||||
Une prerelease intermédiaire peut être ajoutée si le volume réel d'une tranche dépasse le budget. La dernière prerelease reste une tranche de
|
Une prerelease intermédiaire peut être ajoutée si le volume réel d'une tranche dépasse le budget. Après formalisation de `KSP-TRANSPORT-007` en
|
||||||
clôture documentaire/validation et ne doit pas devenir une implémentation massive tardive.
|
`pre.005`, `pre.008` est désormais réservée à un audit rétroactif explicite des wrappers HTTP déjà livrés depuis `0.2.1`, afin de ne pas transformer la
|
||||||
|
tranche de clôture en remédiation fonctionnelle tardive. La dernière prerelease reste une tranche de clôture documentaire/validation et ne doit pas
|
||||||
|
devenir une implémentation massive tardive.
|
||||||
|
|
||||||
## Critères de clôture de `0.2.3`
|
## Critères de clôture de `0.2.3`
|
||||||
|
|
||||||
@@ -683,3 +707,21 @@ La tranche suivante peut commencer par les primitives wire communes sans réouvr
|
|||||||
- aucune nouvelle dépendance Cargo et aucun wrapper `0.2.3` activé prématurément.
|
- aucune nouvelle dépendance Cargo et aucun wrapper `0.2.3` activé prématurément.
|
||||||
|
|
||||||
La tranche suivante reste `pre.003` : `getFeeForMessage`, `getLatestBlockhash`, `getTransactionCount` et `isBlockhashValid`.
|
La tranche suivante reste `pre.003` : `getFeeForMessage`, `getLatestBlockhash`, `getTransactionCount` et `isBlockhashValid`.
|
||||||
|
|
||||||
|
## Résultat de `pre.006`
|
||||||
|
|
||||||
|
`pre.006` active les deux écritures Transaction sans introduire de chemin de soumission parallèle :
|
||||||
|
|
||||||
|
- `requestAirdrop` expose `Pubkey`, lamports, `commitment` et le `recentBlockhash` encore supporté par Agave `v4.2.1` ;
|
||||||
|
- `sendTransaction` transporte une transaction déjà signée/encodée et expose `base58/base64`, `skipPreflight`, `preflightCommitment`, `maxRetries` et
|
||||||
|
`minContextSlot` ;
|
||||||
|
- `sendTransaction.maxRetries` reste explicitement node-side et ne modifie jamais `HttpRetrySettings` ;
|
||||||
|
- les deux wrappers passent par leur descriptor central `WriteSubmission / NeverAfterDispatch` puis `execute_standard_rpc` ;
|
||||||
|
- les fixtures end-to-end comptent les requêtes et prouvent une seule soumission après HTTP 429, HTTP 503 et timeout après dispatch ;
|
||||||
|
- une connexion refusée classée `NotDispatched` peut encore utiliser la policy centrale de retry/fallback, démontrée pour les deux wrappers ;
|
||||||
|
- les erreurs RPC applicatives, dont un échec de preflight `sendTransaction`, restent des erreurs applicatives sans retry transport ;
|
||||||
|
- aucune dépendance transactionnelle/codec supplémentaire n'est ajoutée ;
|
||||||
|
- `simulateTransaction` reste la seule méthode `0.2.3` non encore activée.
|
||||||
|
|
||||||
|
La prévision est ajustée : `pre.008` devient l'audit rétroactif `KSP-TRANSPORT-007` et la clôture finale est déplacée à `pre.009`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user