From 1e1bc0d4216fa966b2ea0ae0ab7b064ed6a7c471 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 18 Aug 2026 12:33:16 +0200 Subject: [PATCH] v0.2.3-pre.005 --- Cargo.toml | 4 +- .../fixtures/http/get_transaction.base58.json | 1 + .../fixtures/http/get_transaction.base64.json | 1 + .../http/get_transaction.binary_legacy.json | 1 + ...transaction.error_unsupported_version.json | 1 + .../fixtures/http/get_transaction.json.json | 1 + .../http/get_transaction.json_parsed.json | 1 + .../fixtures/http/get_transaction.null.json | 1 + .../src/rpc_transactions.rs | 78 +++- .../tests/public_api.rs | 17 +- .../tests/release_completeness.rs | 37 +- .../unit_tests/rpc_transactions.rs | 190 +++++++- deltas/0.2.3/pre.005.md | 408 ++++++++++++++++++ docs/architecture/003-COMPONENT_CONTRACTS.md | 4 +- .../010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md | 27 +- docs/rules/RULES_KSP.md | 3 +- 16 files changed, 758 insertions(+), 17 deletions(-) create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base58.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base64.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.binary_legacy.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.error_unsupported_version.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json_parsed.json create mode 100644 crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.null.json create mode 100644 deltas/0.2.3/pre.005.md diff --git a/Cargo.toml b/Cargo.toml index 7b3aa88..d224e46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 124 +# version: 125 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"] [workspace.package] -version = "0.2.3-pre.4" +version = "0.2.3-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base58.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base58.json new file mode 100644 index 0000000..9342093 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base58.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"slot":431000061,"blockTime":1787000201,"transaction":["3Bxs4NN8M2Yn4TLb","base58"],"meta":null,"version":"legacy","transactionIndex":0},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base64.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base64.json new file mode 100644 index 0000000..2d8e39d --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base64.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"slot":431000062,"blockTime":1787000202,"transaction":["AQIDBAUGBwg=","base64"],"meta":null,"version":0,"transactionIndex":1},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.binary_legacy.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.binary_legacy.json new file mode 100644 index 0000000..fb517da --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.binary_legacy.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"slot":431000060,"blockTime":null,"transaction":"3Bxs4NN8M2Yn4TLb","meta":null,"version":"legacy"},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.error_unsupported_version.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.error_unsupported_version.json new file mode 100644 index 0000000..9091a83 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.error_unsupported_version.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction version (1) is not supported by the requesting client. Please try the request again with the following configuration parameter: \"maxSupportedTransactionVersion\": 1"},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json.json new file mode 100644 index 0000000..b5bb59c --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"slot":431000063,"blockTime":1787000203,"transaction":{"signatures":["fixture-signature"],"message":{"header":{"numRequiredSignatures":1,"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":1},"accountKeys":["11111111111111111111111111111111","SysvarRent111111111111111111111111111111111"],"recentBlockhash":"11111111111111111111111111111111","instructions":[{"programIdIndex":0,"accounts":[0,1],"data":"3Bxs4NN8","stackHeight":1}],"addressTableLookups":null}},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[100000,1],"postBalances":[95000,1],"innerInstructions":[],"logMessages":["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"preTokenBalances":[],"postTokenBalances":[],"rewards":null,"loadedAddresses":{"writable":[],"readonly":[]},"returnData":{"programId":"11111111111111111111111111111111","data":["AQID","base64"]},"computeUnitsConsumed":150,"costUnits":190},"version":0,"transactionIndex":7},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json_parsed.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json_parsed.json new file mode 100644 index 0000000..2c50026 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json_parsed.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":{"slot":431000064,"blockTime":1787000204,"transaction":{"signatures":["fixture-signature-parsed"],"message":{"accountKeys":[{"pubkey":"11111111111111111111111111111111","signer":true,"writable":true,"source":"transaction"},{"pubkey":"SysvarRent111111111111111111111111111111111","signer":false,"writable":false,"source":"transaction"}],"recentBlockhash":"11111111111111111111111111111111","instructions":[{"program":"system","programId":"11111111111111111111111111111111","stackHeight":1,"parsed":{"type":"transfer","info":{"lamports":1,"source":"11111111111111111111111111111111","destination":"SysvarRent111111111111111111111111111111111"}}}]}},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[100000,1],"postBalances":[95000,1],"innerInstructions":null,"logMessages":null,"preTokenBalances":[],"postTokenBalances":[],"rewards":null,"returnData":null,"computeUnitsConsumed":150,"costUnits":190},"version":"legacy","transactionIndex":null},"id":1} diff --git a/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.null.json b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.null.json new file mode 100644 index 0000000..e00e008 --- /dev/null +++ b/crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.null.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","result":null,"id":1} diff --git a/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs b/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs index 3b91a86..0c4b3e0 100644 --- a/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs +++ b/crates/ksp-onchain-transport-lib/src/rpc_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs -// version: 4 +// version: 5 /// Binary encoding accepted for serialized transaction input payloads. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -20,7 +20,6 @@ impl SolanaTransactionBinaryEncoding { }; } - #[cfg(test)] fn from_wire(value: &str) -> std::option::Option { return match value { "base58" => std::option::Option::Some(Self::Base58), @@ -167,7 +166,6 @@ impl SolanaGetTransactionConfig { /// Serializes the modern config to its 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 { @@ -827,7 +825,6 @@ pub enum SolanaEncodedTransaction { impl SolanaEncodedTransaction { /// Decodes the untagged transaction wire union without decoding transaction bytes. - #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { return match value { serde_json::Value::String(data) => std::result::Result::Ok(Self::LegacyBinary(data)), @@ -848,7 +845,6 @@ pub enum SolanaTransactionVersion { } impl SolanaTransactionVersion { - #[cfg(test)] fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { return match value { serde_json::Value::String(value) if value == "legacy" => std::result::Result::Ok(Self::Legacy), @@ -926,7 +922,6 @@ impl SolanaConfirmedTransaction { } /// Decodes one non-null confirmed transaction without decoding Program-specific transaction internals. - #[cfg(test)] pub(crate) fn decode_wire(method: &str, value: serde_json::Value) -> ksp_core_lib::Result { let decoded = crate::decode_wire_json::(method, value); let wire = match decoded { @@ -1251,6 +1246,74 @@ impl crate::HttpTransportPool { }; } + /// Executes the current object-form `getTransaction` request through the common KSP HTTP transport path. + /// + /// `None` omits the optional second parameter. `Some(config)` sends the modern object form exactly, including an empty `{}` object when the + /// caller explicitly supplies an empty modern configuration object. + pub async fn get_transaction( + &self, + role: &crate::HttpRoleName, + signature: &str, + config: std::option::Option<&crate::SolanaGetTransactionConfig>, + ) -> ksp_core_lib::Result> { + if let std::option::Option::Some(config) = config + && config.commitment() == std::option::Option::Some(crate::SolanaCommitment::Processed) + { + return std::result::Result::Err( + ksp_core_lib::Error::new( + crate::ERROR_CODE_INVALID_RPC_PARAMETERS, + "getTransaction commitment must be confirmed or finalized when explicitly provided", + ) + .with_context("rpc_method", "getTransaction") + .with_context("commitment", "processed"), + ); + } + let mut params = std::vec![serde_json::Value::String(signature.to_owned())]; + if let std::option::Option::Some(config) = config { + params.push((*config).to_json_value()); + } + return self.execute_get_transaction(role, params).await; + } + + /// Executes the deprecated bare-encoding `getTransaction` request form retained by Solana RPC for backwards compatibility. + #[deprecated(note = "use HttpTransportPool::get_transaction with SolanaGetTransactionConfig; the bare encoding request form is deprecated")] + pub async fn get_transaction_legacy( + &self, + role: &crate::HttpRoleName, + signature: &str, + encoding: crate::SolanaTransactionEncoding, + ) -> ksp_core_lib::Result> { + ksp_logging_lib::warn!( + target: crate::TRACING_TARGET, + rpc_method = "getTransaction", + request_form = "bare_encoding", + encoding = encoding.as_str(), + "deprecated Solana HTTP RPC request form used" + ); + let params = std::vec![serde_json::Value::String(signature.to_owned()), serde_json::Value::String(encoding.as_str().to_owned()),]; + return self.execute_get_transaction(role, params).await; + } + + async fn execute_get_transaction( + &self, + role: &crate::HttpRoleName, + params: std::vec::Vec, + ) -> ksp_core_lib::Result> { + let value = self.execute_transaction_rpc("getTransaction", role, params).await; + let value = match value { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if value.is_null() { + return std::result::Result::Ok(std::option::Option::None); + } + let transaction = crate::SolanaConfirmedTransaction::decode_wire("getTransaction", value); + return match transaction { + std::result::Result::Ok(transaction) => std::result::Result::Ok(std::option::Option::Some(transaction)), + 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, @@ -1450,7 +1513,6 @@ fn decode_confirmation_status( return std::result::Result::Ok(std::option::Option::Some(status)); } -#[cfg(test)] fn decode_binary_transaction_tuple(method: &str, values: std::vec::Vec) -> ksp_core_lib::Result { if values.len() != 2 { return std::result::Result::Err(invalid_transaction_wire(method, "transaction", "encoded transaction tuple must contain data and encoding")); @@ -1490,7 +1552,6 @@ fn decode_binary_transaction_tuple(method: &str, values: std::vec::Vec, @@ -1597,7 +1658,6 @@ struct WireSignatureStatus { confirmation_status: std::option::Option, } -#[cfg(test)] #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct WireConfirmedTransaction { diff --git a/crates/ksp-onchain-transport-lib/tests/public_api.rs b/crates/ksp-onchain-transport-lib/tests/public_api.rs index 16c3260..708f10c 100644 --- a/crates/ksp-onchain-transport-lib/tests/public_api.rs +++ b/crates/ksp-onchain-transport-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/public_api.rs -// version: 12 +// version: 13 //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. @@ -285,3 +285,18 @@ fn public_transaction_pre_004_additional_read_wrappers_are_available_from_crate_ let _get_signatures_for_address = ksp_onchain_transport_lib::HttpTransportPool::get_signatures_for_address; let _get_signature_statuses = ksp_onchain_transport_lib::HttpTransportPool::get_signature_statuses; } + +#[allow(deprecated)] +#[test] +fn public_transaction_pre_005_get_transaction_complete_request_forms_are_available_from_crate_root() { + let _get_transaction = ksp_onchain_transport_lib::HttpTransportPool::get_transaction; + let _get_transaction_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_legacy; + let config = ksp_onchain_transport_lib::SolanaGetTransactionConfig::new( + std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), + std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed), + std::option::Option::Some(0), + ); + assert_eq!(config.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed)); + assert_eq!(config.encoding(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::JsonParsed)); + assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(0)); +} diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index 141e1f8..f5ca1fe 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 9 +// version: 10 //! Release-level completeness canaries for the `0.2.1` HTTP foundation contract. @@ -291,3 +291,38 @@ fn release_pre_004_transaction_read_subset_is_exact_and_retry_safe() { assert_eq!(actual.len(), 7); assert_eq!(deferred.len(), 4); } + +#[test] +fn release_pre_005_transaction_read_subset_adds_complete_get_transaction_without_advancing_writes_or_simulation() { + let mut expected = std::vec![ + "getFeeForMessage", + "getLatestBlockhash", + "getRecentPrioritizationFees", + "getSignaturesForAddress", + "getSignatureStatuses", + "getTransaction", + "getTransactionCount", + "isBlockhashValid", + ]; + let deferred = std::vec!["requestAirdrop", "sendTransaction", "simulateTransaction"]; + let mut actual = std::vec::Vec::new(); + for method_name in &expected { + let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("pre.005 Transaction descriptor must exist"); + actual.push(descriptor.method()); + 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); + } + let get_transaction = ksp_onchain_transport_lib::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist"); + assert!(get_transaction.request_form_status().has_deprecated_legacy()); + for method_name in &deferred { + let descriptor = ksp_onchain_transport_lib::find_http_rpc_method(method_name).expect("deferred Transaction descriptor must remain registered"); + assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3); + } + actual.sort_unstable(); + expected.sort_unstable(); + assert_eq!(actual, expected); + assert_eq!(actual.len(), 8); + assert_eq!(deferred.len(), 3); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs b/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs index 8362599..08d5bf8 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs -// version: 4 +// version: 5 #[test] fn transaction_encoding_strings_match_current_and_legacy_wire_labels() { @@ -686,3 +686,191 @@ async fn typed_get_signature_statuses_rejects_response_count_mismatch() { 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"); +} diff --git a/deltas/0.2.3/pre.005.md b/deltas/0.2.3/pre.005.md new file mode 100644 index 0000000..00a2aef --- /dev/null +++ b/deltas/0.2.3/pre.005.md @@ -0,0 +1,408 @@ + + + +# Delta `0.2.3-pre.005` — `getTransaction` complet, forme moderne et compatibilité legacy + +## Base requise + +Livraison précédente validée localement par l'opérateur : + +```text +0.2.3-pre.004 +workspace.package.version = "0.2.3-pre.4" +``` + +La validation opérateur du 2026-08-18 a confirmé : + +```text +cargo fmt --all OK +cargo check --workspace OK +cargo clippy --workspace --all-targets OK +cargo test -p ksp-onchain-transport-lib OK +``` + +Résultats Transport de cette base : + +```text +161 unit tests +16 public API tests +10 release completeness tests +0 warning signalé par check/clippy +``` + +## Objectif + +Implémenter `getTransaction` comme huitième wrapper Transaction `Read / RetrySafe` de `0.2.3`, sans réduire la méthode à un sous-ensemble de +convenance. + +La tranche couvre : + +```text +forme moderne objet +forme bare encoding legacy dépréciée +commitment +encoding +maxSupportedTransactionVersion +result = null +transaction chaîne legacy +transaction tuple base58/base64 +transaction JSON/jsonParsed +meta lossless +version legacy/numérique/omise/null +transactionIndex valeur/omission/null +erreurs RPC applicatives +``` + +Après cette tranche : + +```text +8 / 11 wrappers Transactions exécutés +``` + +Restent différés : + +```text +requestAirdrop +sendTransaction +simulateTransaction +``` + +## Règle générale de complétude RPC + +La précision opérateur formulée avant cette tranche devient une règle durable : un wrapper de transport KSP n'est pas considéré complet lorsqu'il +n'expose qu'un sous-ensemble arbitraire des possibilités d'une méthode RPC. + +`KSP-TRANSPORT-007` est ajouté dans `docs/rules/RULES_KSP.md` : + +- tous les paramètres et champs de config audités doivent rester accessibles ; +- les variantes/overloads courants doivent être exposés ; +- les formes legacy encore supportées restent accessibles avec statut explicite ; +- les contraintes déterministes connues sont appliquées localement lorsque KSP peut le faire sans prendre une responsabilité wire étrangère ; +- les variantes, `null` et omissions significatives de réponse sont préservés ; +- un sous-arbre riche peut rester `serde_json::Value` lorsqu'il est conservé losslessly ; +- toute limitation volontaire doit être explicitement documentée. + +`docs/architecture/003-COMPONENT_CONTRACTS.md` est synchronisé avec cette règle. + +Le plan `docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md` passe en version 4 et demande désormais une canarie de complétude sémantique pour les +wrappers de la release, pas uniquement une canarie du nombre de méthodes. + +## Version Cargo + +Conformément à `VER-ID-009` : + +```text +0.2.3-pre.4 -> 0.2.3-pre.5 +``` + +Aucune dépendance ni feature Cargo n'est ajoutée. + +## Surface moderne `getTransaction` + +API publique : + +```text +HttpTransportPool::get_transaction( + role, + signature, + Option<&SolanaGetTransactionConfig>, +) + -> Result> +``` + +La signature reste une chaîne base58 opaque pour Transport : KSP ne prend pas de dépendance `solana-signature` uniquement pour répliquer la +validation du provider. + +Le second paramètre possède trois usages intentionnels : + +```text +None -> second paramètre omis +Some(default config) -> objet moderne explicite {} +Some(config) -> objet moderne avec tous les champs fournis +``` + +Cette distinction permet d'exposer réellement la forme objet moderne sans empêcher la forme sans second paramètre. + +`SolanaGetTransactionConfig` conserve : + +```text +commitment +encoding +maxSupportedTransactionVersion +``` + +La documentation publique courante borne `commitment` à : + +```text +confirmed +finalized +``` + +`processed` est donc rejeté localement avec `ERROR_CODE_INVALID_RPC_PARAMETERS` avant I/O. + +`maxSupportedTransactionVersion` reste un `Option` plutôt qu'une constante figée à `0`, afin de ne pas fermer la surface KSP aux futures +versions wire que le runtime pourra supporter. + +## Encodings complets + +La documentation publique moderne expose : + +```text +base58 +base64 +json +jsonParsed +``` + +Agave `v4.2.1` utilise encore `UiTransactionEncoding` pour `RpcTransactionConfig` et accepte également l'alias historique : + +```text +binary +``` + +KSP conserve donc les cinq labels dans `SolanaTransactionEncoding` : + +```text +Binary +Base58 +Base64 +Json +JsonParsed +``` + +`Binary` est une compatibilité runtime/legacy et n'est pas présenté comme le choix moderne recommandé. + +Les cinq labels sont exercés par le wrapper moderne afin de garantir que KSP ne filtre pas une possibilité supportée par le runtime ciblé. + +## Forme legacy bare encoding + +La documentation Solana courante conserve : + +```text +getTransaction(signature, "") +``` + +mais marque explicitement cette forme bare comme dépréciée. + +KSP expose donc séparément : + +```text +HttpTransportPool::get_transaction_legacy(role, signature, encoding) +``` + +Cette API : + +- est annotée `#[deprecated]` côté Rust ; +- émet un `warn` via `ksp-logging-lib` lorsqu'elle est appelée ; +- sérialise exactement la chaîne d'encoding comme second paramètre ; +- accepte les cinq labels du runtime `v4.2.1` ; +- réutilise exactement le même executor/décodeur que la forme moderne. + +Le wrapper legacy ne crée donc ni nouveau client HTTP ni logique de retry parallèle. + +## Réponse complète et lossless + +`getTransaction` renvoie : + +```text +Option +``` + +`result = null` devient `None` sans être confondu avec une erreur RPC. + +Un résultat présent conserve : + +```text +slot +blockTime +transaction +meta +version +transactionIndex +``` + +### Transaction + +`SolanaEncodedTransaction` devient production-live et conserve les formes Agave : + +```text +LegacyBinary(String) +Binary { data, Base58|Base64 } +Json(serde_json::Value) +``` + +La variante `Json` couvre `json` et `jsonParsed` sans réimplémenter les structures Program-specific du SDK Solana. Aucun champ n'est supprimé. + +### Metadata + +`meta` reste : + +```text +SolanaWireField +``` + +Cette représentation conserve : + +```text +Omitted +Null +Value(object) +``` + +La fixture riche couvre notamment les champs actuels : + +```text +err +status legacy +fee +preBalances +postBalances +innerInstructions +logMessages +preTokenBalances +postTokenBalances +rewards +loadedAddresses +returnData +computeUnitsConsumed +costUnits +``` + +Le choix lossless permet aussi de transporter de nouveaux champs provider/Agave sans les effacer avant qu'un modèle KSP plus spécialisé soit +justifié. + +### Version et transaction index + +`version` conserve : + +```text +Omitted +Null +Legacy +Number(u8) +``` + +`transactionIndex`, présent dans Agave `v4.2.1` mais pas historiquement chez tous les providers, conserve également : + +```text +Omitted +Null +Value(u32) +``` + +Les fixtures préparatoires de `pre.002` qui vérifient déjà ces trois états restent actives. + +## Helpers activés en production + +Seuls les helpers nécessaires à `getTransaction` quittent `#[cfg(test)]` : + +```text +SolanaTransactionBinaryEncoding::from_wire +SolanaGetTransactionConfig::to_json_value +SolanaEncodedTransaction::decode_wire +SolanaTransactionVersion::decode_wire +SolanaConfirmedTransaction::decode_wire +decode_binary_transaction_tuple +decode_transaction_version_field +WireConfirmedTransaction +``` + +Les helpers write/simulation de `pre.006`–`pre.007` restent staged/test-only. Aucun `#[allow(dead_code)]` global n'est ajouté. + +## Fixtures HTTP déterministes ajoutées + +```text +get_transaction.null.json +get_transaction.binary_legacy.json +get_transaction.base58.json +get_transaction.base64.json +get_transaction.json.json +get_transaction.json_parsed.json +get_transaction.error_unsupported_version.json +``` + +Elles couvrent les formes de résultat et les encodings sans décoder les bytes transactionnels. + +## Couverture de tests ajoutée + +Sept tests unitaires HTTP supplémentaires couvrent : + +- second paramètre omis et objet moderne vide explicite ; +- config moderne complète ; +- rejet local de `commitment=processed` ; +- cinq encodings runtime via la forme moderne ; +- cinq encodings via la forme bare legacy dépréciée ; +- préservation d'un `jsonParsed` riche ; +- préservation du JSON brut, du meta courant, de la version et de `transactionIndex` ; +- erreur RPC `unsupported transaction version` préservée comme `RPC_APPLICATION_ERROR`. + +Les tests préparatoires existants continuent en plus à couvrir : + +- tuple avec encoding non binaire rejeté ; +- forme de version inconnue rejetée ; +- `meta/version/transactionIndex` omis/null/présents. + +Un test public compile les deux request forms depuis la crate root. + +Une nouvelle canarie release porte le sous-ensemble Read Transaction exécuté à huit méthodes et vérifie que `getTransaction` conserve +`StableWithDeprecatedLegacy` : + +```text +getFeeForMessage +getLatestBlockhash +getRecentPrioritizationFees +getSignaturesForAddress +getSignatureStatuses +getTransaction +getTransactionCount +isBlockhashValid +``` + +Les trois opérations write/simulation restent différées. + +Après application, la cible Transport attendue devient : + +```text +168 unit tests +17 public API tests +11 release completeness tests +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/rpc_transactions.rs +crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs +crates/ksp-onchain-transport-lib/tests/public_api.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +docs/rules/RULES_KSP.md +docs/architecture/003-COMPONENT_CONTRACTS.md +docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md +``` + +Fichiers ajoutés : + +```text +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.null.json +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.binary_legacy.json +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base58.json +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.base64.json +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json.json +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.json_parsed.json +crates/ksp-onchain-transport-lib/fixtures/http/get_transaction.error_unsupported_version.json +deltas/0.2.3/pre.005.md +``` + +`CHANGELOG.md`, `ROADMAP.md`, `rpc_method.rs`, `executor.rs` et `resilience.rs` restent inchangés. + +## Validations attendues + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +``` + +L'environnement de préparation du delta ne possède pas Cargo/Rustfmt ; ces commandes ne sont donc pas déclarées réussies avant preuve opérateur. diff --git a/docs/architecture/003-COMPONENT_CONTRACTS.md b/docs/architecture/003-COMPONENT_CONTRACTS.md index d0ccf1e..7a58d72 100644 --- a/docs/architecture/003-COMPONENT_CONTRACTS.md +++ b/docs/architecture/003-COMPONENT_CONTRACTS.md @@ -1,5 +1,5 @@ - + # Contrats initiaux des composants KSP @@ -66,6 +66,8 @@ Il ne dépend pas de Config, Store ou Program. Pour toute surface normative ciblée, toutes les méthodes documentées sont inventoriées/implémentées sauf impossibilité documentée. Les méthodes deprecated/obsolete encore fonctionnelles et unstable/experimental émettent un warning KSP à l'utilisation. +La complétude vaut aussi à l'intérieur de chaque opération : paramètres, options, overloads et formes legacy supportées sont exposés, et les variantes de réponse sont préservées sans perte. KSP n'est pas tenu de dupliquer un SDK externe lorsque des sous-arbres wire lossless suffisent. + ## Transport off-chain Aucune `ksp-offchain-transport-api` commune n'est prévue. diff --git a/docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md b/docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md index b07d7b0..17f5ead 100644 --- a/docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md +++ b/docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.2.3` — HTTP Transactions @@ -217,6 +217,30 @@ fige donc pas artificiellement le type public sur une liste de versions sériali Pour la **sortie** `getTransaction`, le wire doit rester plus large : `base58`, `base64`, `json`, `jsonParsed`, ainsi que les formes legacy encore acceptées/auditées. Les formes encodées et JSON ne doivent pas être écrasées dans un seul `String` ambigu. +## Règle de complétude des wrappers RPC + +À partir de `pre.005`, la règle générale `KSP-TRANSPORT-007` explicite un principe déjà recherché par les releases HTTP précédentes : un wrapper KSP +n'est pas considéré complet s'il ne couvre qu'un sous-ensemble de convenance de la méthode RPC. Il doit exposer toute la surface sémantique auditée : + +```text +paramètres obligatoires et optionnels +champs de configuration +variantes/overloads courants +formes legacy encore supportées +contraintes déterministes connues +variantes/null/omissions significatives des réponses +``` + +Cette exigence ne signifie pas recopier `solana-rpc-client` ou tout `solana-transaction-status-client-types`. Une structure riche peut rester un +`serde_json::Value` à une frontière explicitement lossless lorsque KSP n'a pas encore besoin de son modèle métier, à condition qu'aucune possibilité +RPC ni information wire ne soit supprimée. Les syntaxes strictement équivalentes peuvent être canonicalisées. + +Pour `getTransaction`, `pre.005` couvre donc les deux formes de requête auditée : objet moderne et bare encoding legacy déprécié. La documentation +publique courante expose `base58`, `base64`, `json` et `jsonParsed` pour l'objet moderne ; Agave `v4.2.1` accepte aussi l'alias rétrocompatible +`binary` via le même enum wire, que KSP conserve donc sans le présenter comme choix moderne recommandé. La config complète +`commitment/encoding/maxSupportedTransactionVersion` est exposée, avec rejet local de `processed` car la méthode documente `confirmed|finalized`. Le résultat +`null`, les transactions chaîne/tuple/objet, les versions `legacy`/numériques, `meta` lossless et `transactionIndex` optionnel courant sont préservés. + ## Matrice exacte des 11 méthodes | Méthode | Paramètres ordonnés / config | Résultat à préserver | Validation / particularités | @@ -586,6 +610,7 @@ clôture documentaire/validation et ne doit pas devenir une implémentation mass La release ne peut être candidate stable que si : - les 11 wrappers sont publics et passent tous par `execute_standard_rpc` ; +- chaque wrapper couvre tous les paramètres/options/overloads normatifs ou runtime supportés retenus par l'audit, selon `KSP-TRANSPORT-007` ; - les trois classes de sécurité restent exactes `8 Read / 2 WriteSubmission / 1 Simulation` ; - `requestAirdrop` et `sendTransaction` prouvent l'absence de resend automatique après dispatch ambigu ; - `getTransaction` préserve la forme moderne et la compatibilité legacy explicitement dépréciée ; diff --git a/docs/rules/RULES_KSP.md b/docs/rules/RULES_KSP.md index 25bc747..5d20623 100644 --- a/docs/rules/RULES_KSP.md +++ b/docs/rules/RULES_KSP.md @@ -1,5 +1,5 @@ - + # Règles spécifiques à KSP @@ -255,5 +255,6 @@ - **KSP-REL-015** — Une application Tauri complétée ne crée `PRESENTATION.md` que si elle possède réellement une vue de présentation embarquée ; dans ce cas le fichier est finalisé comme contenu UI sans liens navigables et reste distinct du `README.md` et du `USAGE.md`. - **KSP-REL-016** — Une prerelease vise environ 15 à 20 minutes de travail effectif. Le `pre.001` dimensionne aussi la release concrète entière : une release doit pouvoir être ouverte, développée, validée et clôturée dans une seule session de chat. Si cette clôture paraît incertaine, la release est scindée avant l'implémentation fonctionnelle lourde ; une version volontairement répartie sur plusieurs sessions est interdite. - **KSP-TRANSPORT-006** — Pour une surface de transport explicitement ciblée, KSP inventorie et implémente toutes les méthodes/opérations exposées par la documentation normative retenue, sauf impossibilité technique explicitement documentée. L'inventaire couvre aussi les sections officielles séparées `deprecated`/`obsolete` et `unstable`/`experimental` lorsqu'elles existent. Les opérations deprecated/obsolete encore réellement fonctionnelles et unstable/experimental restent utilisables mais émettent un `warn` via `ksp-logging-lib` à chaque utilisation concernée ; leur statut est décrit par une metadata centralisée et non par des warnings dispersés. +- **KSP-TRANSPORT-007** — La complétude d'un wrapper de transport standard couvre toute la surface sémantique de requête auditée : paramètres, options de configuration, variantes/overloads courants, formes legacy encore supportées et contraintes déterministes connues. Les formes de réponse pertinentes sont conservées losslessly, y compris les variantes, `null` et omissions significatives. KSP peut canonicaliser des syntaxes strictement équivalentes et conserver des sous-arbres wire riches via `serde_json::Value` tant qu'aucune information n'est perdue ; toute limitation volontaire d'une possibilité normative/runtime supportée doit être explicitement justifiée et documentée. - **KSP-FLOW-001** — La progression durable canonique est `RAW -> CORE -> DECODE -> SPECIALIZED`. RAW et CORE ne nécessitent aucun decoder Program ; le passage RAW -> CORE reste une normalisation générique de la blockchain Solana. À partir de DECODE, KSP progresse verticalement par groupe fonctionnel à travers wire, décodage, matérialisation, projection spécialisée si utile, préparation d'exécution, policy, exécution et scénarios de validation. - **KSP-FLOW-002** — Un programme ou composant satellite nécessaire à la compréhension, la matérialisation ou l'exécution correcte d'un protocole appartient au groupe de ce protocole. Il n'est pas reporté artificiellement dans une catégorie `trading-adjacent`.