// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs // version: 4 #[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 { let role = crate::HttpEndpointRoleSettings::new( crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], 10, crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), ); let endpoint = crate::HttpEndpointSettings::new( "fixture", true, crate::HttpProviderName::new("fixture"), crate::HttpClusterName::new("local"), crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"), std::time::Duration::from_secs(1), std::time::Duration::from_secs(1), std::option::Option::Some(1), std::vec![role], ); let settings = crate::HttpTransportSettings::new( std::vec![endpoint], crate::HttpRetrySettings::new(0, std::time::Duration::from_millis(1), std::time::Duration::from_millis(1)), ); 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 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"); }