// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs // version: 9 #[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, } let omitted = serde_json::from_value::(serde_json::json!({})).expect("omitted fixture must decode"); let null = serde_json::from_value::(serde_json::json!({"field": null})).expect("null fixture must decode"); let value = serde_json::from_value::(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::().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::(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::>(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::>(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::>(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::>(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::>(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::(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) { 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::().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::().expect("fixture pubkey must parse"); let second = "SysvarRent111111111111111111111111111111111".parse::().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::().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::().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::().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::().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::::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::().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::().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::().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::().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::().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::().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::().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::().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::().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")); }