Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
2026-08-18 16:12:07 +02:00

1368 lines
79 KiB
Rust

// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
// version: 8
#[test]
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
assert_eq!(crate::SolanaTransactionBinaryEncoding::Base58.as_str(), "base58");
assert_eq!(crate::SolanaTransactionBinaryEncoding::Base64.as_str(), "base64");
assert_eq!(crate::SolanaTransactionEncoding::Binary.as_str(), "binary");
assert_eq!(crate::SolanaTransactionEncoding::Base58.as_str(), "base58");
assert_eq!(crate::SolanaTransactionEncoding::Base64.as_str(), "base64");
assert_eq!(crate::SolanaTransactionEncoding::Json.as_str(), "json");
assert_eq!(crate::SolanaTransactionEncoding::JsonParsed.as_str(), "jsonParsed");
}
#[test]
fn wire_field_preserves_omitted_null_and_value_states() {
#[derive(serde::Deserialize)]
struct Holder {
#[serde(default)]
field: crate::SolanaWireField<u64>,
}
let omitted = serde_json::from_value::<Holder>(serde_json::json!({})).expect("omitted fixture must decode");
let null = serde_json::from_value::<Holder>(serde_json::json!({"field": null})).expect("null fixture must decode");
let value = serde_json::from_value::<Holder>(serde_json::json!({"field": 7})).expect("value fixture must decode");
assert!(omitted.field.is_omitted());
assert!(!omitted.field.is_null());
assert_eq!(omitted.field.value(), std::option::Option::None);
assert!(null.field.is_null());
assert!(!null.field.is_omitted());
assert_eq!(null.field.value(), std::option::Option::None);
assert_eq!(value.field.value(), std::option::Option::Some(&7));
}
#[test]
fn get_transaction_config_serializes_modern_object_without_legacy_overload() {
let config = crate::SolanaGetTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
std::option::Option::Some(0),
);
assert!(!config.is_empty());
assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"confirmed","encoding":"jsonParsed","maxSupportedTransactionVersion":0}));
assert!(crate::SolanaGetTransactionConfig::default().is_empty());
assert_eq!(crate::SolanaGetTransactionConfig::default().to_json_value(), serde_json::json!({}));
}
#[test]
fn signatures_for_address_config_preserves_pagination_and_context_order_independently() {
let config = crate::SolanaSignaturesForAddressConfig::new(
std::option::Option::Some("before-signature".to_owned()),
std::option::Option::Some("until-signature".to_owned()),
std::option::Option::Some(25),
std::option::Option::Some(crate::SolanaCommitment::Finalized),
std::option::Option::Some(430_000_000),
);
assert_eq!(config.before(), std::option::Option::Some("before-signature"));
assert_eq!(config.until(), std::option::Option::Some("until-signature"));
assert_eq!(config.limit(), std::option::Option::Some(25));
assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized));
assert_eq!(config.min_context_slot(), std::option::Option::Some(430_000_000));
assert_eq!(
config.to_json_value(),
serde_json::json!({
"before":"before-signature",
"until":"until-signature",
"limit":25,
"commitment":"finalized",
"minContextSlot":430000000
})
);
assert!(crate::SolanaSignaturesForAddressConfig::default().is_empty());
}
#[test]
fn signature_status_and_airdrop_configs_preserve_explicit_options() {
let status = crate::SolanaSignatureStatusesConfig::new(std::option::Option::Some(false));
assert!(!status.is_empty());
assert_eq!(status.search_transaction_history(), std::option::Option::Some(false));
assert_eq!(status.to_json_value(), serde_json::json!({"searchTransactionHistory":false}));
assert!(crate::SolanaSignatureStatusesConfig::default().is_empty());
let airdrop = crate::SolanaRequestAirdropConfig::new(
std::option::Option::Some("recent-blockhash".to_owned()),
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
);
assert!(!airdrop.is_empty());
assert_eq!(airdrop.recent_blockhash(), std::option::Option::Some("recent-blockhash"));
assert_eq!(airdrop.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed));
assert_eq!(airdrop.to_json_value(), serde_json::json!({"recentBlockhash":"recent-blockhash","commitment":"confirmed"}));
assert!(crate::SolanaRequestAirdropConfig::default().is_empty());
}
#[test]
fn send_transaction_config_keeps_node_retries_distinct_from_transport_policy() {
let config = crate::SolanaSendTransactionConfig::new(
std::option::Option::Some(true),
std::option::Option::Some(crate::SolanaCommitment::Processed),
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(4),
std::option::Option::Some(430_000_000),
);
assert_eq!(config.skip_preflight(), std::option::Option::Some(true));
assert_eq!(config.max_retries(), std::option::Option::Some(4));
assert_eq!(
config.to_json_value(),
serde_json::json!({
"skipPreflight":true,
"preflightCommitment":"processed",
"encoding":"base64",
"maxRetries":4,
"minContextSlot":430000000
})
);
assert!(crate::SolanaSendTransactionConfig::default().is_empty());
}
#[test]
fn simulate_transaction_config_serializes_accounts_and_flags_without_decoding_transaction() {
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(crate::SolanaAccountEncoding::Base64), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(true),
std::option::Option::Some(false),
std::option::Option::Some(430_000_000),
std::option::Option::Some(true),
std::option::Option::Some(accounts),
);
assert_eq!(config.encoding(), std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64));
assert_eq!(config.replace_recent_blockhash(), std::option::Option::Some(true));
assert_eq!(config.sig_verify(), std::option::Option::Some(false));
assert_eq!(config.accounts().expect("accounts config must be present").addresses(), &[address]);
assert_eq!(
config.to_json_value(),
serde_json::json!({
"commitment":"confirmed",
"encoding":"base64",
"replaceRecentBlockhash":true,
"sigVerify":false,
"minContextSlot":430000000,
"innerInstructions":true,
"accounts":{"encoding":"base64","addresses":["11111111111111111111111111111111"]}
})
);
assert!(crate::SolanaSimulateTransactionConfig::default().is_empty());
}
#[test]
fn latest_blockhash_fixture_decodes_shared_result() {
let value =
serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/latest_blockhash.v4_2_1.json")).expect("latest blockhash fixture must decode");
let blockhash = crate::SolanaLatestBlockhash::decode_wire("getLatestBlockhash", value).expect("latest blockhash must decode");
assert_eq!(blockhash.blockhash(), "11111111111111111111111111111111");
assert_eq!(blockhash.last_valid_block_height(), 430_123_456);
}
#[test]
fn latest_blockhash_rejects_empty_wire_value() {
let result = crate::SolanaLatestBlockhash::decode_wire("getLatestBlockhash", serde_json::json!({"blockhash":"","lastValidBlockHeight":430123456}));
assert_eq!(result.expect_err("empty blockhash must fail closed").code(), crate::ERROR_CODE_INVALID_RESPONSE);
}
#[test]
fn prioritization_fee_fixture_preserves_server_order() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/prioritization_fee.v4_2_1.json"))
.expect("prioritization fee fixture must decode");
let first = crate::SolanaPrioritizationFee::decode_wire("getRecentPrioritizationFees", values[0].clone()).expect("first fee must decode");
let second = crate::SolanaPrioritizationFee::decode_wire("getRecentPrioritizationFees", values[1].clone()).expect("second fee must decode");
assert_eq!(first.slot(), 430_000_123);
assert_eq!(first.prioritization_fee(), 1_000);
assert_eq!(second.slot(), 430_000_122);
assert_eq!(second.prioritization_fee(), 0);
}
#[test]
fn signature_info_fixture_accepts_current_transaction_index_and_older_omission() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/signature_info.v4_2_1.json"))
.expect("signature info fixture must decode");
let current = crate::SolanaSignatureInfo::decode_wire("getSignaturesForAddress", values[0].clone()).expect("current signature info must decode");
let older = crate::SolanaSignatureInfo::decode_wire("getSignaturesForAddress", values[1].clone()).expect("older signature info must decode");
assert_eq!(current.slot(), 430_000_123);
assert_eq!(current.memo(), std::option::Option::Some("fixture memo"));
assert_eq!(current.transaction_index(), std::option::Option::Some(7));
assert_eq!(current.confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Finalized));
assert_eq!(older.transaction_index(), std::option::Option::None);
assert!(older.err().is_some());
assert_eq!(older.confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Confirmed));
}
#[test]
fn signature_status_fixture_preserves_position_null_and_legacy_status_value() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/signature_status.variants.json"))
.expect("signature status fixture must decode");
assert!(values[0].is_null());
let rooted = crate::SolanaSignatureStatus::decode_wire("getSignatureStatuses", values[1].clone()).expect("rooted signature status must decode");
let errored = crate::SolanaSignatureStatus::decode_wire("getSignatureStatuses", values[2].clone()).expect("errored signature status must decode");
assert_eq!(rooted.confirmations(), std::option::Option::None);
assert_eq!(rooted.status(), &serde_json::json!({"Ok":null}));
assert_eq!(rooted.confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Finalized));
assert_eq!(errored.confirmations(), std::option::Option::Some(3));
assert!(errored.err().is_some());
}
#[test]
fn unknown_confirmation_status_is_rejected_as_invalid_response() {
let result = crate::SolanaSignatureStatus::decode_wire(
"getSignatureStatuses",
serde_json::json!({"slot":1,"confirmations":0,"status":{"Ok":null},"err":null,"confirmationStatus":"future-status"}),
);
let error = result.expect_err("unknown confirmation status must fail closed");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
}
#[test]
fn encoded_transaction_fixture_preserves_legacy_binary_tuple_and_json_shapes() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/encoded_transaction.variants.json"))
.expect("encoded transaction fixture must decode");
let legacy = crate::SolanaEncodedTransaction::decode_wire("getTransaction", values[0].clone()).expect("legacy transaction must decode");
let binary = crate::SolanaEncodedTransaction::decode_wire("getTransaction", values[1].clone()).expect("binary transaction must decode");
let json = crate::SolanaEncodedTransaction::decode_wire("getTransaction", values[2].clone()).expect("JSON transaction must decode");
assert!(matches!(legacy, crate::SolanaEncodedTransaction::LegacyBinary(_)));
assert!(matches!(binary, crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }));
assert!(matches!(json, crate::SolanaEncodedTransaction::Json(_)));
let invalid = crate::SolanaEncodedTransaction::decode_wire("getTransaction", serde_json::json!(["payload", "json"]));
assert_eq!(invalid.expect_err("non-binary tuple encoding must fail").code(), crate::ERROR_CODE_INVALID_RESPONSE);
}
#[test]
fn confirmed_transaction_fixture_preserves_meta_version_and_transaction_index_states() {
let values = serde_json::from_str::<std::vec::Vec<serde_json::Value>>(include_str!("../fixtures/http/confirmed_transaction.variants.json"))
.expect("confirmed transaction fixture must decode");
let current = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", values[0].clone()).expect("current transaction must decode");
let legacy = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", values[1].clone()).expect("legacy transaction must decode");
let omitted = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", values[2].clone()).expect("omitted-field transaction must decode");
assert_eq!(current.slot(), 430_000_123);
assert_eq!(current.version().value(), std::option::Option::Some(&crate::SolanaTransactionVersion::Number(0)));
assert_eq!(current.transaction_index().value(), std::option::Option::Some(&7));
assert!(current.meta().value().is_some());
assert_eq!(legacy.version().value(), std::option::Option::Some(&crate::SolanaTransactionVersion::Legacy));
assert!(legacy.meta().is_null());
assert!(legacy.transaction_index().is_null());
assert!(omitted.meta().is_omitted());
assert!(omitted.version().is_omitted());
assert!(omitted.transaction_index().is_omitted());
}
#[test]
fn confirmed_transaction_rejects_unknown_version_shape() {
let result = crate::SolanaConfirmedTransaction::decode_wire(
"getTransaction",
serde_json::json!({"slot":1,"blockTime":null,"transaction":"legacy","meta":null,"version":"v0"}),
);
assert_eq!(result.expect_err("unknown version label must fail").code(), crate::ERROR_CODE_INVALID_RESPONSE);
}
#[test]
fn simulation_result_fixture_preserves_rich_optional_fields_and_positional_null_accounts() {
let value = serde_json::from_str::<serde_json::Value>(include_str!("../fixtures/http/simulate_transaction_result.v4_2_1.json"))
.expect("simulation result fixture must decode");
let result = crate::SolanaSimulateTransactionResult::decode_wire("simulateTransaction", value).expect("simulation result must decode");
assert!(result.err().is_null());
assert_eq!(result.logs().value().expect("logs must be present").len(), 2);
let accounts = result.accounts().value().expect("accounts must be present");
assert_eq!(accounts.len(), 2);
assert_eq!(accounts[0].as_ref().expect("first account must be present").lamports(), 1_000);
assert!(accounts[1].is_none());
assert_eq!(result.units_consumed().value(), std::option::Option::Some(&150));
assert_eq!(result.loaded_accounts_data_size().value(), std::option::Option::Some(&64));
assert_eq!(result.fee().value(), std::option::Option::Some(&5_000));
let replacement = result.replacement_blockhash().value().expect("replacement blockhash must be present");
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
assert!(result.return_data().value().is_some());
assert!(result.inner_instructions().value().is_some());
assert!(result.pre_token_balances().value().is_some());
assert!(result.post_token_balances().value().is_some());
assert!(result.loaded_addresses().value().is_some());
}
#[test]
fn simulation_result_distinguishes_omitted_from_explicit_null_fields() {
let result = crate::SolanaSimulateTransactionResult::decode_wire("simulateTransaction", serde_json::json!({"err":null,"logs":null,"unitsConsumed":1}))
.expect("partial simulation result must decode");
assert!(result.err().is_null());
assert!(result.logs().is_null());
assert_eq!(result.units_consumed().value(), std::option::Option::Some(&1));
assert!(result.accounts().is_omitted());
assert!(result.replacement_blockhash().is_omitted());
assert!(result.loaded_addresses().is_omitted());
}
fn transaction_pool_for_url(url: &str) -> crate::HttpTransportPool {
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(
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");
}
fn serve_transaction_once(body: &'static str) -> (std::string::String, std::thread::JoinHandle<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 one request");
let request = read_transaction_request(&mut stream);
let response = format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body,);
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
return request;
});
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 serve_transaction_status_then_body(
status_line: &'static str,
body: &'static str,
) -> (std::string::String, std::thread::JoinHandle<(std::string::String, 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 first_stream, _) = listener.accept().expect("fixture server must accept first request");
let first_request = read_transaction_request(&mut first_stream);
let first_response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
std::io::Write::write_all(&mut first_stream, first_response.as_bytes()).expect("fixture first response must write");
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
return loop {
match listener.accept() {
std::result::Result::Ok((mut retry_stream, _)) => {
let retry_request = read_transaction_request(&mut retry_stream);
let retry_response =
format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body,);
std::io::Write::write_all(&mut retry_stream, retry_response.as_bytes()).expect("fixture retry response must write");
break (first_request, retry_request);
},
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
if std::time::Instant::now() >= deadline {
panic!("fixture server did not receive retry before deadline");
}
std::thread::sleep(std::time::Duration::from_millis(5));
},
std::result::Result::Err(error) => panic!("fixture listener failed while waiting for retry: {error}"),
}
};
});
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];
loop {
let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read");
if count == 0 {
break;
}
bytes.extend_from_slice(&buffer[..count]);
if transaction_request_complete(bytes.as_slice()) {
break;
}
}
return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8");
}
fn transaction_request_complete(bytes: &[u8]) -> bool {
let text = match std::str::from_utf8(bytes) {
std::result::Result::Ok(text) => text,
std::result::Result::Err(_) => return false,
};
let header_end = match text.find("\r\n\r\n") {
std::option::Option::Some(value) => value,
std::option::Option::None => return false,
};
let mut content_length = 0_usize;
for line in text[..header_end].lines() {
let (name, value) = match line.split_once(':') {
std::option::Option::Some(parts) => parts,
std::option::Option::None => continue,
};
if name.eq_ignore_ascii_case("content-length") {
content_length = value.trim().parse::<usize>().expect("content length must parse");
}
}
return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length);
}
fn transaction_request_body(request: &str) -> serde_json::Value {
let body = request.split("\r\n\r\n").nth(1).expect("fixture request body must exist");
return serde_json::from_str(body).expect("fixture request body must be JSON");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_fee_for_message_serializes_exact_params_and_decodes_contextual_fee() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_fee_for_message.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(431_000_000));
let response = pool
.get_fee_for_message(&crate::HttpRoleName::new("default"), "AQIDBA==", std::option::Option::Some(&config))
.await
.expect("getFeeForMessage fixture must succeed");
assert_eq!(response.context().slot(), 431_000_001);
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
assert_eq!(response.value(), &std::option::Option::Some(5_000));
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getFeeForMessage"));
assert_eq!(body["params"], serde_json::json!(["AQIDBA==", {"commitment":"confirmed","minContextSlot":431000000}]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_fee_for_message_preserves_null_and_omits_absent_config() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_fee_for_message.null.json"));
let pool = transaction_pool_for_url(url.as_str());
let response = pool
.get_fee_for_message(&crate::HttpRoleName::new("default"), "opaque-message", std::option::Option::None)
.await
.expect("null fee fixture must succeed");
assert_eq!(response.value(), &std::option::Option::None);
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-message"]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_fee_for_message_preserves_rpc_application_error() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_fee_for_message.error.json"));
let pool = transaction_pool_for_url(url.as_str());
let result = pool.get_fee_for_message(&crate::HttpRoleName::new("default"), "not-decoded-locally", std::option::Option::None).await;
let error = result.expect_err("remote invalid message must remain an RPC application error");
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
handle.join().expect("fixture server must join");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_latest_blockhash_omits_empty_config_and_decodes_contextual_result() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_latest_blockhash.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaContextConfig::default();
let response = pool
.get_latest_blockhash(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
.await
.expect("getLatestBlockhash fixture must succeed");
assert_eq!(response.context().slot(), 431_000_003);
assert_eq!(response.value().blockhash(), "11111111111111111111111111111111");
assert_eq!(response.value().last_valid_block_height(), 431_123_456);
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_latest_blockhash_rejects_empty_blockhash_from_http_response() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_latest_blockhash.invalid_empty.json"));
let pool = transaction_pool_for_url(url.as_str());
let result = pool.get_latest_blockhash(&crate::HttpRoleName::new("default"), std::option::Option::None).await;
let error = result.expect_err("empty latest blockhash must fail typed decoding");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
handle.join().expect("fixture server must join");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_count_serializes_context_config_and_decodes_u64() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction_count.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::None);
let count = pool
.get_transaction_count(&crate::HttpRoleName::new("default"), std::option::Option::Some(&config))
.await
.expect("getTransactionCount fixture must succeed");
assert_eq!(count, 987_654_321);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getTransactionCount"));
assert_eq!(body["params"], serde_json::json!([{"commitment":"finalized"}]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_is_blockhash_valid_preserves_opaque_blockhash_and_contextual_bool() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/is_blockhash_valid.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Processed), std::option::Option::Some(431_000_004));
let response = pool
.is_blockhash_valid(&crate::HttpRoleName::new("default"), "opaque-blockhash", std::option::Option::Some(&config))
.await
.expect("isBlockhashValid fixture must succeed");
assert_eq!(response.context().slot(), 431_000_005);
assert_eq!(response.value(), &true);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("isBlockhashValid"));
assert_eq!(body["params"], serde_json::json!(["opaque-blockhash", {"commitment":"processed","minContextSlot":431000004}]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_recent_prioritization_fees_serializes_addresses_and_preserves_server_order() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let first = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let second = "SysvarRent111111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let first_text = first.to_string();
let second_text = second.to_string();
let accounts = std::vec![first, second];
let fees = pool
.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice()))
.await
.expect("getRecentPrioritizationFees fixture must succeed");
assert_eq!(fees.len(), 2);
assert_eq!(fees[0].slot(), 431_000_030);
assert_eq!(fees[0].prioritization_fee(), 1_200);
assert_eq!(fees[1].slot(), 431_000_029);
assert_eq!(fees[1].prioritization_fee(), 0);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getRecentPrioritizationFees"));
assert_eq!(body["params"], serde_json::json!([[first_text, second_text]]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_rejects_more_than_128_before_io() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let fees = pool
.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::None)
.await
.expect("getRecentPrioritizationFees without addresses must succeed");
assert_eq!(fees.len(), 2);
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([]));
let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let max_accounts = std::vec![account; 128];
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
let max_pool = transaction_pool_for_url(url.as_str());
let fees = max_pool
.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(max_accounts.as_slice()))
.await
.expect("exactly 128 prioritization-fee accounts must remain accepted");
assert_eq!(fees.len(), 2);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["params"][0].as_array().expect("address parameter must be an array").len(), 128);
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
let accounts = std::vec![account; 129];
let result = invalid_pool.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice())).await;
let error = result.expect_err("more than 128 prioritization-fee accounts must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].key(), "account_count");
assert_eq!(error.context()[1].value(), "129");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signatures_for_address_serializes_pagination_and_preserves_newest_first_result() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signatures_for_address.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let config = crate::SolanaSignaturesForAddressConfig::new(
std::option::Option::Some("before-signature".to_owned()),
std::option::Option::Some("until-signature".to_owned()),
std::option::Option::Some(1_000),
std::option::Option::Some(crate::SolanaCommitment::Finalized),
std::option::Option::Some(431_000_000),
);
let signatures = pool
.get_signatures_for_address(&crate::HttpRoleName::new("default"), &address, std::option::Option::Some(&config))
.await
.expect("getSignaturesForAddress fixture must succeed");
assert_eq!(signatures.len(), 2);
assert_eq!(signatures[0].signature(), "newest-signature");
assert_eq!(signatures[0].slot(), 431_000_040);
assert_eq!(signatures[0].memo(), std::option::Option::Some("memo-newest"));
assert_eq!(signatures[0].block_time(), std::option::Option::Some(1_787_000_100));
assert_eq!(signatures[0].confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Finalized));
assert_eq!(signatures[0].transaction_index(), std::option::Option::Some(9));
assert_eq!(signatures[1].signature(), "older-signature");
assert!(signatures[1].err().is_some());
assert_eq!(signatures[1].memo(), std::option::Option::None);
assert_eq!(signatures[1].block_time(), std::option::Option::None);
assert_eq!(signatures[1].transaction_index(), std::option::Option::None);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getSignaturesForAddress"));
assert_eq!(
body["params"],
serde_json::json!([
address.to_string(),
{
"before":"before-signature",
"until":"until-signature",
"limit":1000,
"commitment":"finalized",
"minContextSlot":431000000
}
])
);
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signatures_for_address_rejects_limit_outside_1_to_1000_before_io() {
let pool = transaction_pool_for_url("http://127.0.0.1:9");
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
for limit in [0_usize, 1_001_usize] {
let config = crate::SolanaSignaturesForAddressConfig::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(limit),
std::option::Option::None,
std::option::Option::None,
);
let result = pool.get_signatures_for_address(&crate::HttpRoleName::new("default"), &address, std::option::Option::Some(&config)).await;
let error = result.expect_err("out-of-range getSignaturesForAddress limit must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].key(), "limit");
assert_eq!(error.context()[1].value(), limit.to_string());
}
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signatures_for_address_preserves_rpc_application_error() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signatures_for_address.error.json"));
let pool = transaction_pool_for_url(url.as_str());
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let result = pool.get_signatures_for_address(&crate::HttpRoleName::new("default"), &address, std::option::Option::None).await;
let error = result.expect_err("remote transaction-history error must remain an RPC application error");
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
handle.join().expect("fixture server must join");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signature_statuses_preserves_positional_nulls_and_history_config() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let signatures = std::vec!["missing-signature".to_owned(), "finalized-signature".to_owned(), "confirmed-signature".to_owned()];
let config = crate::SolanaSignatureStatusesConfig::new(std::option::Option::Some(true));
let response = pool
.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::Some(&config))
.await
.expect("getSignatureStatuses fixture must succeed");
assert_eq!(response.context().slot(), 431_000_050);
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
assert_eq!(response.value().len(), 3);
assert!(response.value()[0].is_none());
let finalized = response.value()[1].as_ref().expect("second positional status must be present");
assert_eq!(finalized.slot(), 431_000_049);
assert_eq!(finalized.confirmations(), std::option::Option::None);
assert_eq!(finalized.status(), &serde_json::json!({"Ok":null}));
assert_eq!(finalized.confirmation_status(), std::option::Option::Some(crate::SolanaTransactionConfirmationStatus::Finalized));
let confirmed = response.value()[2].as_ref().expect("third positional status must be present");
assert_eq!(confirmed.confirmations(), std::option::Option::Some(2));
assert!(confirmed.err().is_some());
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getSignatureStatuses"));
assert_eq!(body["params"], serde_json::json!([signatures, {"searchTransactionHistory":true}]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signature_statuses_accepts_empty_input_and_omits_empty_config() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.empty.json"));
let pool = transaction_pool_for_url(url.as_str());
let signatures = std::vec::Vec::<std::string::String>::new();
let config = crate::SolanaSignatureStatusesConfig::default();
let response = pool
.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::Some(&config))
.await
.expect("empty getSignatureStatuses input must remain accepted");
assert!(response.value().is_empty());
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([[]]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signature_statuses_accepts_256_and_rejects_more_than_256_before_io() {
let signatures = std::vec!["signature".to_owned(); 256];
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.max_256.json"));
let pool = transaction_pool_for_url(url.as_str());
let response = pool
.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None)
.await
.expect("exactly 256 signatures must remain accepted");
assert_eq!(response.value().len(), 256);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["params"][0].as_array().expect("signature parameter must be an array").len(), 256);
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
let signatures = std::vec!["signature".to_owned(); 257];
let result = invalid_pool.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None).await;
let error = result.expect_err("more than 256 signatures must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].key(), "signature_count");
assert_eq!(error.context()[1].value(), "257");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_signature_statuses_rejects_response_count_mismatch() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_signature_statuses.count_mismatch.json"));
let pool = transaction_pool_for_url(url.as_str());
let requested = std::vec!["first".to_owned(), "second".to_owned()];
let result = pool.get_signature_statuses(&crate::HttpRoleName::new("default"), requested.as_slice(), std::option::Option::None).await;
let error = result.expect_err("status response count mismatch must reject typed decoding");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
assert_eq!(error.context()[1].key(), "expected_count");
assert_eq!(error.context()[1].value(), "2");
assert_eq!(error.context()[2].key(), "actual_count");
assert_eq!(error.context()[2].value(), "1");
handle.join().expect("fixture server must join");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_modern_supports_omitted_and_explicit_empty_config() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json"));
let pool = transaction_pool_for_url(url.as_str());
let result = pool
.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::None)
.await
.expect("getTransaction without config must succeed");
assert!(result.is_none());
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["fixture-signature"]));
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaGetTransactionConfig::default();
let result = pool
.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config))
.await
.expect("getTransaction with explicit empty modern config must succeed");
assert!(result.is_none());
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["fixture-signature", {}]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_modern_serializes_full_config_and_preserves_rich_json_parsed_wire() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.json_parsed.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaGetTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
std::option::Option::Some(0),
);
let result = pool
.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config))
.await
.expect("modern getTransaction fixture must succeed")
.expect("modern getTransaction fixture must contain a transaction");
assert_eq!(result.slot(), 431_000_064);
assert_eq!(result.block_time(), std::option::Option::Some(1_787_000_204));
assert_eq!(result.version().value(), std::option::Option::Some(&crate::SolanaTransactionVersion::Legacy));
assert!(result.transaction_index().is_null());
let transaction = match result.transaction() {
crate::SolanaEncodedTransaction::Json(value) => value,
_ => panic!("jsonParsed response must remain a JSON object"),
};
assert_eq!(transaction["message"]["accountKeys"][0]["source"], serde_json::json!("transaction"));
assert_eq!(transaction["message"]["instructions"][0]["parsed"]["type"], serde_json::json!("transfer"));
let meta = result.meta().value().expect("metadata must be present");
assert_eq!(meta["computeUnitsConsumed"], serde_json::json!(150));
assert_eq!(meta["costUnits"], serde_json::json!(190));
assert!(meta["loadedAddresses"].is_null());
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getTransaction"));
assert_eq!(
body["params"],
serde_json::json!([
"fixture-signature",
{"commitment":"confirmed","encoding":"jsonParsed","maxSupportedTransactionVersion":0}
])
);
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_modern_covers_all_agave_v4_2_1_encoding_labels_and_response_shapes() {
let cases = [
(crate::SolanaTransactionEncoding::Binary, include_str!("../fixtures/http/get_transaction.binary_legacy.json")),
(crate::SolanaTransactionEncoding::Base58, include_str!("../fixtures/http/get_transaction.base58.json")),
(crate::SolanaTransactionEncoding::Base64, include_str!("../fixtures/http/get_transaction.base64.json")),
(crate::SolanaTransactionEncoding::Json, include_str!("../fixtures/http/get_transaction.json.json")),
(crate::SolanaTransactionEncoding::JsonParsed, include_str!("../fixtures/http/get_transaction.json_parsed.json")),
];
for (encoding, fixture) in cases {
let (url, handle) = serve_transaction_once(fixture);
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaGetTransactionConfig::new(std::option::Option::None, std::option::Option::Some(encoding), std::option::Option::None);
let result = pool
.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config))
.await
.expect("encoding-specific modern getTransaction must succeed")
.expect("encoding-specific modern getTransaction must contain a transaction");
match encoding {
crate::SolanaTransactionEncoding::Binary => assert!(matches!(result.transaction(), crate::SolanaEncodedTransaction::LegacyBinary(_))),
crate::SolanaTransactionEncoding::Base58 => assert!(matches!(
result.transaction(),
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base58, .. }
)),
crate::SolanaTransactionEncoding::Base64 => assert!(matches!(
result.transaction(),
crate::SolanaEncodedTransaction::Binary { encoding: crate::SolanaTransactionBinaryEncoding::Base64, .. }
)),
crate::SolanaTransactionEncoding::Json | crate::SolanaTransactionEncoding::JsonParsed => {
assert!(matches!(result.transaction(), crate::SolanaEncodedTransaction::Json(_)));
},
}
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["params"], serde_json::json!(["fixture-signature", {"encoding":encoding.as_str()}]));
}
}
#[allow(deprecated)]
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_legacy_covers_all_bare_encoding_labels() {
let cases = [
(crate::SolanaTransactionEncoding::Binary, include_str!("../fixtures/http/get_transaction.binary_legacy.json")),
(crate::SolanaTransactionEncoding::Base58, include_str!("../fixtures/http/get_transaction.base58.json")),
(crate::SolanaTransactionEncoding::Base64, include_str!("../fixtures/http/get_transaction.base64.json")),
(crate::SolanaTransactionEncoding::Json, include_str!("../fixtures/http/get_transaction.json.json")),
(crate::SolanaTransactionEncoding::JsonParsed, include_str!("../fixtures/http/get_transaction.json_parsed.json")),
];
for (encoding, fixture) in cases {
let (url, handle) = serve_transaction_once(fixture);
let pool = transaction_pool_for_url(url.as_str());
let result = pool
.get_transaction_legacy(&crate::HttpRoleName::new("default"), "fixture-signature", encoding)
.await
.expect("legacy bare-encoding getTransaction must succeed");
assert!(result.is_some());
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getTransaction"));
assert_eq!(body["params"], serde_json::json!(["fixture-signature", encoding.as_str()]));
}
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_preserves_raw_json_meta_version_and_transaction_index() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.json.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaGetTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Finalized),
std::option::Option::Some(crate::SolanaTransactionEncoding::Json),
std::option::Option::Some(0),
);
let result = pool
.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config))
.await
.expect("raw JSON getTransaction must succeed")
.expect("raw JSON getTransaction must contain a transaction");
assert_eq!(result.version().value(), std::option::Option::Some(&crate::SolanaTransactionVersion::Number(0)));
assert_eq!(result.transaction_index().value(), std::option::Option::Some(&7));
let meta = result.meta().value().expect("metadata must be present");
assert_eq!(meta["status"], serde_json::json!({"Ok":null}));
assert_eq!(meta["loadedAddresses"]["writable"], serde_json::json!([]));
assert_eq!(meta["returnData"]["data"], serde_json::json!(["AQID", "base64"]));
assert_eq!(meta["costUnits"], serde_json::json!(190));
let request = handle.join().expect("fixture server must join");
assert_eq!(
transaction_request_body(request.as_str())["params"],
serde_json::json!([
"fixture-signature",
{"commitment":"finalized","encoding":"json","maxSupportedTransactionVersion":0}
])
);
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_preserves_unsupported_version_rpc_error() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.error_unsupported_version.json"));
let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaGetTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
std::option::Option::Some(0),
);
let result = pool.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config)).await;
let error = result.expect_err("unsupported transaction version must remain an RPC application error");
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
handle.join().expect("fixture server must join");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_get_transaction_rejects_processed_commitment_before_io() {
let pool = transaction_pool_for_url("http://127.0.0.1:9");
let config = crate::SolanaGetTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Processed),
std::option::Option::Some(crate::SolanaTransactionEncoding::Json),
std::option::Option::None,
);
let result = pool.get_transaction(&crate::HttpRoleName::new("default"), "fixture-signature", std::option::Option::Some(&config)).await;
let error = result.expect_err("processed getTransaction commitment must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
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"));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_serializes_complete_config_and_preserves_rich_result() {
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(crate::SolanaAccountEncoding::Base64), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(true),
std::option::Option::Some(false),
std::option::Option::Some(431_000_000),
std::option::Option::Some(true),
std::option::Option::Some(accounts),
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let response = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-base64-transaction", std::option::Option::Some(&config))
.await
.expect("simulateTransaction rich fixture must succeed");
assert_eq!(response.context().slot(), 431_000_030);
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
assert!(response.value().err().is_null());
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&150));
assert_eq!(response.value().loaded_accounts_data_size().value(), std::option::Option::Some(&64));
assert_eq!(response.value().fee().value(), std::option::Option::Some(&5_000));
assert_eq!(response.value().pre_balances().value(), std::option::Option::Some(&std::vec![10_000, 0]));
assert_eq!(response.value().post_balances().value(), std::option::Option::Some(&std::vec![5_000, 5_000]));
let returned_accounts = response.value().accounts().value().expect("simulation accounts must be present");
assert_eq!(returned_accounts.len(), 2);
assert!(returned_accounts[0].is_some());
assert!(returned_accounts[1].is_none());
let replacement = response.value().replacement_blockhash().value().expect("replacement blockhash must be present");
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
assert_eq!(replacement.last_valid_block_height(), 430_123_999);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("simulateTransaction"));
assert_eq!(
body["params"],
serde_json::json!([
"opaque-base64-transaction",
{
"commitment":"confirmed",
"encoding":"base64",
"replaceRecentBlockhash":true,
"sigVerify":false,
"minContextSlot":431000000,
"innerInstructions":true,
"accounts":{"encoding":"base64","addresses":["11111111111111111111111111111111"]}
}
])
);
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_supports_base58_and_omits_empty_config() {
let base58 = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base58),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let _ = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-base58-transaction", std::option::Option::Some(&base58))
.await
.expect("simulateTransaction 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::SolanaSimulateTransactionConfig::default();
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let _ = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-default-transaction", std::option::Option::Some(&empty))
.await
.expect("simulateTransaction 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_simulate_transaction_supports_all_current_account_return_encodings() {
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let encodings = [crate::SolanaAccountEncoding::Base64, crate::SolanaAccountEncoding::Base64Zstd, crate::SolanaAccountEncoding::JsonParsed];
for encoding in encodings {
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(encoding), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(accounts),
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let _ = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&config))
.await
.expect("supported simulation account encoding must execute");
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["params"][1]["accounts"]["encoding"], serde_json::json!(encoding.as_str()));
}
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_rejects_deterministic_invalid_configs_before_io() {
let pool = transaction_pool_for_url(unused_local_url().as_str());
let conflicting = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(true),
std::option::Option::Some(true),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&conflicting)).await;
assert_eq!(result.expect_err("conflicting simulation flags must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58];
for encoding in rejected_encodings {
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(encoding), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(accounts),
);
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&config)).await;
assert_eq!(result.expect_err("unsupported simulation account encoding must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
}
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_preserves_partial_null_and_omitted_result() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let response = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None)
.await
.expect("partial simulateTransaction fixture must succeed");
handle.join().expect("fixture server must join");
assert!(response.value().err().is_null());
assert!(response.value().logs().is_null());
assert!(response.value().accounts().is_null());
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&99));
assert!(response.value().loaded_accounts_data_size().is_omitted());
assert!(response.value().return_data().is_null());
assert!(response.value().inner_instructions().is_omitted());
assert!(response.value().replacement_blockhash().is_null());
assert!(response.value().fee().is_omitted());
assert!(response.value().loaded_addresses().is_omitted());
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_preserves_rpc_application_error() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.error.json"));
let pool = transaction_pool_for_url(url.as_str());
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "invalid-opaque-transaction", std::option::Option::None).await;
assert_eq!(result.expect_err("simulation RPC 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_simulate_transaction_retries_temporary_http_failure_safely() {
let (url, handle) = serve_transaction_status_then_body("503 Service Unavailable", include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 1);
let response = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-retry-safe-transaction", std::option::Option::None)
.await
.expect("retry-safe simulation must recover after temporary HTTP failure");
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&99));
let (first, second) = handle.join().expect("retry fixture server must join");
assert_eq!(transaction_request_body(first.as_str())["method"], serde_json::json!("simulateTransaction"));
assert_eq!(transaction_request_body(second.as_str())["method"], serde_json::json!("simulateTransaction"));
}