v0.2.3-pre.006

This commit is contained in:
2026-08-18 12:35:29 +02:00
parent 1e1bc0d421
commit 4439589f0d
12 changed files with 728 additions and 39 deletions

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: supplied blockhash is not valid"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":"airdrop-fixture-signature-111111111111111111111111111111111111111111111111","id":1}

View File

@@ -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}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":"send-fixture-signature-22222222222222222222222222222222222222222222222222","id":1}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 14
// version: 15
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![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 ->
//! 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
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and seven read wrappers through `pre.004`,
//! including bounded prioritization-fee, address-signature and signature-status queries. `getTransaction`, both write submissions and
//! `simulateTransaction`, plus the
//! `0.2.4` family remain staged.
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives, all eight read wrappers and both write
//! submissions through `pre.006`, including complete modern/legacy `getTransaction` coverage and centralized no-resend protection for writes.
//! `simulateTransaction` and the `0.2.4` family remain staged.
mod client;
mod constants;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 5
// version: 6
/// Binary encoding accepted for serialized transaction input payloads.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -321,14 +321,12 @@ impl SolanaRequestAirdropConfig {
}
/// Returns whether the airdrop config would serialize to an empty object.
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
return self.recent_blockhash.is_none() && self.commitment.is_none();
}
/// Serializes this config to the Solana JSON-RPC wire object.
#[must_use]
#[cfg(test)]
pub(crate) fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new();
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.
#[cfg(test)]
pub(crate) const fn is_empty(&self) -> bool {
return self.skip_preflight.is_none()
&& self.preflight_commitment.is_none()
@@ -406,7 +403,6 @@ impl SolanaSendTransactionConfig {
/// Serializes this config to the Solana JSON-RPC wire object.
#[must_use]
#[cfg(test)]
pub(crate) fn to_json_value(self) -> serde_json::Value {
let mut object = serde_json::Map::new();
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.
pub async fn get_transaction_count(
&self,

View File

@@ -1,5 +1,5 @@
// 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.
@@ -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.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));
}

View File

@@ -1,5 +1,5 @@
// 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.
@@ -326,3 +326,36 @@ fn release_pre_005_transaction_read_subset_adds_complete_get_transaction_without
assert_eq!(actual.len(), 8);
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);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
// version: 5
// version: 6
#[test]
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 {
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),
);
let endpoint = crate::HttpEndpointSettings::new(
"fixture",
true,
crate::HttpProviderName::new("fixture"),
crate::HttpClusterName::new("local"),
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
std::option::Option::Some(1),
std::vec![role],
);
return transaction_pool_for_urls(&[(url, 10)], std::time::Duration::from_secs(1), 0);
}
fn transaction_pool_for_urls(urls: &[(&str, u32)], request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
let mut endpoints = std::vec::Vec::with_capacity(urls.len());
for (index, (url, priority)) in urls.iter().enumerate() {
let role = crate::HttpEndpointRoleSettings::new(
crate::HttpRoleName::new("default"),
true,
std::vec![crate::HttpRequestKind::wildcard()],
*priority,
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
);
endpoints.push(crate::HttpEndpointSettings::new(
format!("fixture-{index}"),
true,
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(
std::vec![endpoint],
crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)),
endpoints,
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");
}
@@ -328,6 +335,69 @@ fn serve_transaction_once(body: &'static str) -> (std::string::String, std::thre
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 {
let mut bytes = std::vec::Vec::new();
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].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"));
}