v0.2.3-pre.006
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user