v0.2.3-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
#[test]
|
||||
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
||||
@@ -391,6 +391,42 @@ fn serve_transaction_timeout_and_count(delay: std::time::Duration) -> (std::stri
|
||||
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");
|
||||
@@ -1139,3 +1175,193 @@ async fn typed_write_wrappers_can_retry_when_connection_failure_proves_not_dispa
|
||||
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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user