v0.2.3-pre.007

This commit is contained in:
2026-08-18 16:12:07 +02:00
parent fb32ed03f8
commit 704110d509
11 changed files with 782 additions and 19 deletions

View File

@@ -0,0 +1,8 @@
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "invalid transaction: failed to sanitize accounts offsets correctly"
},
"id": 1
}

View File

@@ -0,0 +1,18 @@
{
"jsonrpc": "2.0",
"result": {
"context": {
"slot": 431000031,
"apiVersion": "4.2.1"
},
"value": {
"err": null,
"logs": null,
"accounts": null,
"unitsConsumed": 99,
"returnData": null,
"replacementBlockhash": null
}
},
"id": 1
}

View File

@@ -0,0 +1,60 @@
{
"jsonrpc": "2.0",
"result": {
"context": {
"slot": 431000030,
"apiVersion": "4.2.1"
},
"value": {
"err": null,
"logs": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"accounts": [
{
"lamports": 1000,
"data": [
"",
"base64"
],
"owner": "11111111111111111111111111111111",
"executable": false,
"rentEpoch": 0,
"space": 0
},
null
],
"unitsConsumed": 150,
"loadedAccountsDataSize": 64,
"returnData": {
"programId": "11111111111111111111111111111111",
"data": [
"AQI=",
"base64"
]
},
"innerInstructions": [],
"replacementBlockhash": {
"blockhash": "ComputeBudget111111111111111111111111111111",
"lastValidBlockHeight": 430123999
},
"fee": 5000,
"preBalances": [
10000,
0
],
"postBalances": [
5000,
5000
],
"preTokenBalances": [],
"postTokenBalances": [],
"loadedAddresses": {
"writable": [],
"readonly": []
}
}
},
"id": 1
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 15
// version: 16
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -10,9 +10,9 @@
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config ->
//! Transport adapter without creating a reverse dependency. Logical endpoint clients, priority-aware pools, bounded admission limits and retry/no-resend policy
//! are available. The four typed Solana HTTP foundation canaries plus all 22 typed `0.2.2` Accounts, Tokens and Cluster wrappers execute real JSON-RPC
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives, all eight read wrappers and both write
//! submissions through `pre.006`, including complete modern/legacy `getTransaction` coverage and centralized no-resend protection for writes.
//! `simulateTransaction` and the `0.2.4` family remain staged.
//! requests through the shared transport path. `0.2.3` exposes its shared Transaction wire/config primitives and all eleven Transaction wrappers through
//! `pre.007`: eight reads, two write submissions with centralized no-resend protection, and retry-safe `simulateTransaction`, including complete
//! modern/legacy `getTransaction` coverage. The `0.2.4` family remains staged.
mod client;
mod constants;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 6
// version: 7
/// Binary encoding accepted for serialized transaction input payloads.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -452,7 +452,6 @@ impl SolanaSimulationAccountsConfig {
/// Serializes the nested account config to the Solana JSON-RPC wire object.
#[must_use]
#[cfg(test)]
pub(crate) fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(encoding) = self.encoding {
@@ -545,7 +544,6 @@ impl SolanaSimulateTransactionConfig {
}
/// Returns whether the simulation config would serialize to an empty object.
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
return self.commitment.is_none()
&& self.encoding.is_none()
@@ -558,7 +556,6 @@ impl SolanaSimulateTransactionConfig {
/// Serializes this config to the Solana JSON-RPC wire object.
#[must_use]
#[cfg(test)]
pub(crate) fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(commitment) = self.commitment {
@@ -1050,7 +1047,6 @@ impl SolanaSimulateTransactionResult {
}
/// Decodes a simulation result while preserving optional/nullable current Agave fields.
#[cfg(test)]
pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<Self> {
let decoded = crate::decode_wire_json::<WireSimulateTransactionResult>(method, value);
let wire = match decoded {
@@ -1357,6 +1353,65 @@ impl crate::HttpTransportPool {
};
}
/// Executes typed `simulateTransaction` through the common KSP HTTP transport path.
///
/// Transport forwards the already encoded transaction as opaque text. The wrapper enforces only deterministic RPC invariants that do not require
/// decoding transaction bytes: `sigVerify` cannot be combined with `replaceRecentBlockhash`, and simulation account-return encoding cannot use the
/// legacy `binary` / `base58` account encodings rejected by the targeted Agave runtime.
pub async fn simulate_transaction(
&self,
role: &crate::HttpRoleName,
transaction: &str,
config: std::option::Option<&crate::SolanaSimulateTransactionConfig>,
) -> ksp_core_lib::Result<crate::SolanaRpcResponse<crate::SolanaSimulateTransactionResult>> {
if let std::option::Option::Some(config) = config {
if config.sig_verify() == std::option::Option::Some(true) && config.replace_recent_blockhash() == std::option::Option::Some(true) {
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
"simulateTransaction sigVerify may not be used with replaceRecentBlockhash",
)
.with_context("rpc_method", "simulateTransaction"),
);
}
if let std::option::Option::Some(accounts) = config.accounts()
&& let std::option::Option::Some(encoding) = accounts.encoding()
&& (encoding == crate::SolanaAccountEncoding::Binary || encoding == crate::SolanaAccountEncoding::Base58)
{
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
"simulateTransaction account-return encoding must be base64, base64+zstd, or jsonParsed",
)
.with_context("rpc_method", "simulateTransaction")
.with_context("accounts_encoding", encoding.as_str()),
);
}
}
let mut params = std::vec![serde_json::Value::String(transaction.to_owned())];
if let std::option::Option::Some(config) = config
&& !config.is_empty()
{
params.push(config.to_json_value());
}
let value = self.execute_transaction_rpc("simulateTransaction", role, params).await;
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let contextual = decode_transaction_contextual_wire("simulateTransaction", value);
let (context, value) = match contextual {
std::result::Result::Ok(contextual) => contextual,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = crate::SolanaSimulateTransactionResult::decode_wire("simulateTransaction", value);
return match result {
std::result::Result::Ok(result) => std::result::Result::Ok(crate::SolanaRpcResponse::new(context, result)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Executes typed `getTransactionCount` through the common KSP HTTP transport path.
pub async fn get_transaction_count(
&self,
@@ -1612,7 +1667,6 @@ fn decode_transaction_version_field(
};
}
#[cfg(test)]
fn decode_simulation_accounts_field(
method: &str,
field: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
@@ -1640,7 +1694,6 @@ fn decode_simulation_accounts_field(
return std::result::Result::Ok(crate::SolanaWireField::Value(accounts));
}
#[cfg(test)]
fn decode_replacement_blockhash_field(
method: &str,
field: crate::SolanaWireField<serde_json::Value>,
@@ -1716,7 +1769,6 @@ struct WireConfirmedTransaction {
transaction_index: crate::SolanaWireField<u32>,
}
#[cfg(test)]
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSimulateTransactionResult {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 14
// version: 15
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -321,3 +321,29 @@ fn public_transaction_pre_006_write_wrappers_and_complete_configs_are_available_
assert_eq!(send.max_retries(), std::option::Option::Some(5));
assert_eq!(send.min_context_slot(), std::option::Option::Some(431_000_000));
}
#[test]
fn public_transaction_pre_007_simulation_wrapper_and_complete_config_are_available_from_crate_root() {
let _simulate_transaction = ksp_onchain_transport_lib::HttpTransportPool::simulate_transaction;
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let accounts = ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
std::vec![address],
);
let config = ksp_onchain_transport_lib::SolanaSimulateTransactionConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(false),
std::option::Option::Some(true),
std::option::Option::Some(431_000_000),
std::option::Option::Some(true),
std::option::Option::Some(accounts),
);
assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed));
assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64));
assert_eq!(config.sig_verify(), std::option::Option::Some(true));
assert_eq!(
config.accounts().and_then(ksp_onchain_transport_lib::SolanaSimulationAccountsConfig::encoding),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64Zstd),
);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 11
// version: 12
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
@@ -196,7 +196,7 @@ fn release_pre_007_v0_2_2_exact_set_is_complete_without_advancing_future_release
}
#[test]
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact_before_wrapper_completion() {
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact() {
let mut expected = std::vec![
"getFeeForMessage",
"getLatestBlockhash",
@@ -359,3 +359,44 @@ fn release_pre_006_transaction_subset_adds_both_write_submissions_without_advanc
assert_eq!(reads.len(), 8);
assert_eq!(writes.len(), 2);
}
#[test]
fn release_pre_007_completes_all_v0_2_3_transaction_wrappers_with_exact_safety_classes() {
let reads = std::vec![
"getFeeForMessage",
"getLatestBlockhash",
"getRecentPrioritizationFees",
"getSignaturesForAddress",
"getSignatureStatuses",
"getTransaction",
"getTransactionCount",
"isBlockhashValid",
];
let writes = std::vec!["requestAirdrop", "sendTransaction"];
let simulations = std::vec!["simulateTransaction"];
for method_name in &reads {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 read descriptor must exist");
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Read);
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
}
for method_name in &writes {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 write descriptor must exist");
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission);
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
}
for method_name in &simulations {
let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.007 simulation descriptor must exist");
assert_eq!(descriptor.category(), ksp_onchain_transport_lib::HttpRpcCategory::Transactions);
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3);
assert_eq!(descriptor.operation_kind(), ksp_onchain_transport_lib::RpcOperationKind::Simulation);
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
}
assert_eq!(reads.len(), 8);
assert_eq!(writes.len(), 2);
assert_eq!(simulations.len(), 1);
assert_eq!(reads.len() + writes.len() + simulations.len(), 11);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
// version: 7
// version: 8
#[test]
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
@@ -391,6 +391,42 @@ fn serve_transaction_timeout_and_count(delay: std::time::Duration) -> (std::stri
return (format!("http://{address}"), handle);
}
fn serve_transaction_status_then_body(
status_line: &'static str,
body: &'static str,
) -> (std::string::String, std::thread::JoinHandle<(std::string::String, std::string::String)>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
let address = listener.local_addr().expect("fixture listener address must resolve");
let handle = std::thread::spawn(move || {
let (mut first_stream, _) = listener.accept().expect("fixture server must accept first request");
let first_request = read_transaction_request(&mut first_stream);
let first_response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
std::io::Write::write_all(&mut first_stream, first_response.as_bytes()).expect("fixture first response must write");
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
return loop {
match listener.accept() {
std::result::Result::Ok((mut retry_stream, _)) => {
let retry_request = read_transaction_request(&mut retry_stream);
let retry_response =
format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body,);
std::io::Write::write_all(&mut retry_stream, retry_response.as_bytes()).expect("fixture retry response must write");
break (first_request, retry_request);
},
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
if std::time::Instant::now() >= deadline {
panic!("fixture server did not receive retry before deadline");
}
std::thread::sleep(std::time::Duration::from_millis(5));
},
std::result::Result::Err(error) => panic!("fixture listener failed while waiting for retry: {error}"),
}
};
});
return (format!("http://{address}"), handle);
}
fn unused_local_url() -> std::string::String {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("unused-port probe must bind");
let address = listener.local_addr().expect("unused-port probe address must resolve");
@@ -1139,3 +1175,193 @@ async fn typed_write_wrappers_can_retry_when_connection_failure_proves_not_dispa
let request = handle.join().expect("secondary fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("sendTransaction"));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_serializes_complete_config_and_preserves_rich_result() {
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(crate::SolanaAccountEncoding::Base64), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(true),
std::option::Option::Some(false),
std::option::Option::Some(431_000_000),
std::option::Option::Some(true),
std::option::Option::Some(accounts),
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.success.json"));
let pool = transaction_pool_for_url(url.as_str());
let response = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-base64-transaction", std::option::Option::Some(&config))
.await
.expect("simulateTransaction rich fixture must succeed");
assert_eq!(response.context().slot(), 431_000_030);
assert_eq!(response.context().api_version(), std::option::Option::Some("4.2.1"));
assert!(response.value().err().is_null());
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&150));
assert_eq!(response.value().loaded_accounts_data_size().value(), std::option::Option::Some(&64));
assert_eq!(response.value().fee().value(), std::option::Option::Some(&5_000));
assert_eq!(response.value().pre_balances().value(), std::option::Option::Some(&std::vec![10_000, 0]));
assert_eq!(response.value().post_balances().value(), std::option::Option::Some(&std::vec![5_000, 5_000]));
let returned_accounts = response.value().accounts().value().expect("simulation accounts must be present");
assert_eq!(returned_accounts.len(), 2);
assert!(returned_accounts[0].is_some());
assert!(returned_accounts[1].is_none());
let replacement = response.value().replacement_blockhash().value().expect("replacement blockhash must be present");
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
assert_eq!(replacement.last_valid_block_height(), 430_123_999);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("simulateTransaction"));
assert_eq!(
body["params"],
serde_json::json!([
"opaque-base64-transaction",
{
"commitment":"confirmed",
"encoding":"base64",
"replaceRecentBlockhash":true,
"sigVerify":false,
"minContextSlot":431000000,
"innerInstructions":true,
"accounts":{"encoding":"base64","addresses":["11111111111111111111111111111111"]}
}
])
);
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_supports_base58_and_omits_empty_config() {
let base58 = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base58),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let _ = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-base58-transaction", std::option::Option::Some(&base58))
.await
.expect("simulateTransaction base58 fixture must succeed");
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
let empty = crate::SolanaSimulateTransactionConfig::default();
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let _ = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-default-transaction", std::option::Option::Some(&empty))
.await
.expect("simulateTransaction empty config must canonicalize to omission");
let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-default-transaction"]));
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_supports_all_current_account_return_encodings() {
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let encodings = [crate::SolanaAccountEncoding::Base64, crate::SolanaAccountEncoding::Base64Zstd, crate::SolanaAccountEncoding::JsonParsed];
for encoding in encodings {
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(encoding), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(accounts),
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let _ = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&config))
.await
.expect("supported simulation account encoding must execute");
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
assert_eq!(body["params"][1]["accounts"]["encoding"], serde_json::json!(encoding.as_str()));
}
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_rejects_deterministic_invalid_configs_before_io() {
let pool = transaction_pool_for_url(unused_local_url().as_str());
let conflicting = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(true),
std::option::Option::Some(true),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&conflicting)).await;
assert_eq!(result.expect_err("conflicting simulation flags must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58];
for encoding in rejected_encodings {
let accounts = crate::SolanaSimulationAccountsConfig::new(std::option::Option::Some(encoding), std::vec![address]);
let config = crate::SolanaSimulateTransactionConfig::new(
std::option::Option::None,
std::option::Option::Some(crate::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(accounts),
);
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&config)).await;
assert_eq!(result.expect_err("unsupported simulation account encoding must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
}
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_preserves_partial_null_and_omitted_result() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str());
let response = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None)
.await
.expect("partial simulateTransaction fixture must succeed");
handle.join().expect("fixture server must join");
assert!(response.value().err().is_null());
assert!(response.value().logs().is_null());
assert!(response.value().accounts().is_null());
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&99));
assert!(response.value().loaded_accounts_data_size().is_omitted());
assert!(response.value().return_data().is_null());
assert!(response.value().inner_instructions().is_omitted());
assert!(response.value().replacement_blockhash().is_null());
assert!(response.value().fee().is_omitted());
assert!(response.value().loaded_addresses().is_omitted());
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_preserves_rpc_application_error() {
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.error.json"));
let pool = transaction_pool_for_url(url.as_str());
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "invalid-opaque-transaction", std::option::Option::None).await;
assert_eq!(result.expect_err("simulation RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
handle.join().expect("fixture server must join");
}
#[tokio::test(flavor = "current_thread")]
async fn typed_simulate_transaction_retries_temporary_http_failure_safely() {
let (url, handle) = serve_transaction_status_then_body("503 Service Unavailable", include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 1);
let response = pool
.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-retry-safe-transaction", std::option::Option::None)
.await
.expect("retry-safe simulation must recover after temporary HTTP failure");
assert_eq!(response.value().units_consumed().value(), std::option::Option::Some(&99));
let (first, second) = handle.join().expect("retry fixture server must join");
assert_eq!(transaction_request_body(first.as_str())["method"], serde_json::json!("simulateTransaction"));
assert_eq!(transaction_request_body(second.as_str())["method"], serde_json::json!("simulateTransaction"));
}