v0.2.3-pre.002

This commit is contained in:
2026-08-18 11:13:04 +02:00
parent d06284fca2
commit 1ccf11bcc1
15 changed files with 2130 additions and 12 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 120 # version: 121
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.2.3-pre.1" version = "0.2.3-pre.2"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,36 @@
[
{
"slot": 430000123,
"blockTime": 1787000000,
"transaction": ["AQIDBAUGBwg=", "base64"],
"meta": {
"err": null,
"status": {"Ok": null},
"fee": 5000,
"preBalances": [10000],
"postBalances": [5000]
},
"version": 0,
"transactionIndex": 7
},
{
"slot": 430000100,
"blockTime": null,
"transaction": "3Bxs4NN8M2Yn4TLb",
"meta": null,
"version": "legacy",
"transactionIndex": null
},
{
"slot": 430000050,
"blockTime": null,
"transaction": {
"signatures": [],
"message": {
"accountKeys": [],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": []
}
}
}
]

View File

@@ -0,0 +1,12 @@
[
"3Bxs4NN8M2Yn4TLb",
["AQIDBAUGBwg=", "base64"],
{
"signatures": ["5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5"],
"message": {
"accountKeys": [],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": []
}
}
]

View File

@@ -0,0 +1,4 @@
{
"blockhash": "11111111111111111111111111111111",
"lastValidBlockHeight": 430123456
}

View File

@@ -0,0 +1,4 @@
[
{"slot": 430000123, "prioritizationFee": 1000},
{"slot": 430000122, "prioritizationFee": 0}
]

View File

@@ -0,0 +1,19 @@
[
{
"signature": "5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5",
"slot": 430000123,
"err": null,
"memo": "fixture memo",
"blockTime": 1787000000,
"confirmationStatus": "finalized",
"transactionIndex": 7
},
{
"signature": "4Nd1mC5QfVhNq8xqP7z8d7K6a5m4n3b2v1c9x8z7y6w5u4t3s2r1q9p8o7n6m5L4K3J2H1G9F8E7D6C5B4A3",
"slot": 430000100,
"err": {"InstructionError": [0, "Custom"]},
"memo": null,
"blockTime": null,
"confirmationStatus": "confirmed"
}
]

View File

@@ -0,0 +1,17 @@
[
null,
{
"slot": 430000123,
"confirmations": null,
"status": {"Ok": null},
"err": null,
"confirmationStatus": "finalized"
},
{
"slot": 430000120,
"confirmations": 3,
"status": {"Err": {"InstructionError": [0, "Custom"]}},
"err": {"InstructionError": [0, "Custom"]},
"confirmationStatus": "confirmed"
}
]

View File

@@ -0,0 +1,35 @@
{
"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": []
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs // file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 11 // version: 12
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -10,7 +10,8 @@
//! independent from `ksp-config-lib`, Store and Program layers. `ksp-config-lib` now constructs these public settings through its one-way Config -> //! 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 //! 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 //! 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 while the `0.2.3` and `0.2.4` audited families remain staged. //! requests through the shared transport path. `0.2.3` now exposes its shared Transaction wire/config primitives while its 11 typed wrappers remain staged;
//! the `0.2.4` family remains fully staged.
mod client; mod client;
mod constants; mod constants;
@@ -25,6 +26,7 @@ mod rpc_cluster;
mod rpc_common; mod rpc_common;
mod rpc_method; mod rpc_method;
mod rpc_tokens; mod rpc_tokens;
mod rpc_transactions;
mod settings; mod settings;
pub(crate) use self::constants::TRACING_TARGET; pub(crate) use self::constants::TRACING_TARGET;
@@ -197,6 +199,44 @@ pub use self::rpc_tokens::SolanaTokenAccountBalance;
pub use self::rpc_tokens::SolanaTokenAccountSelector; pub use self::rpc_tokens::SolanaTokenAccountSelector;
/// Token amount returned by Solana HTTP token RPC methods. /// Token amount returned by Solana HTTP token RPC methods.
pub use self::rpc_tokens::SolanaTokenAmount; pub use self::rpc_tokens::SolanaTokenAmount;
/// Confirmed transaction result returned by `getTransaction` when the RPC result is non-null.
pub use self::rpc_transactions::SolanaConfirmedTransaction;
/// Wire-preserving transaction payload returned by `getTransaction`.
pub use self::rpc_transactions::SolanaEncodedTransaction;
/// Modern configuration object accepted by `getTransaction`.
pub use self::rpc_transactions::SolanaGetTransactionConfig;
/// Blockhash information returned by `getLatestBlockhash` and optionally by simulation.
pub use self::rpc_transactions::SolanaLatestBlockhash;
/// One recent prioritization-fee sample returned by `getRecentPrioritizationFees`.
pub use self::rpc_transactions::SolanaPrioritizationFee;
/// Configuration accepted by `requestAirdrop`.
pub use self::rpc_transactions::SolanaRequestAirdropConfig;
/// Configuration accepted by `sendTransaction` without changing KSP transport retry semantics.
pub use self::rpc_transactions::SolanaSendTransactionConfig;
/// One ordered signature record returned by `getSignaturesForAddress`.
pub use self::rpc_transactions::SolanaSignatureInfo;
/// One non-null position returned by `getSignatureStatuses`.
pub use self::rpc_transactions::SolanaSignatureStatus;
/// Optional historical-search configuration accepted by `getSignatureStatuses`.
pub use self::rpc_transactions::SolanaSignatureStatusesConfig;
/// Pagination and context configuration accepted by `getSignaturesForAddress`.
pub use self::rpc_transactions::SolanaSignaturesForAddressConfig;
/// Configuration accepted by `simulateTransaction`.
pub use self::rpc_transactions::SolanaSimulateTransactionConfig;
/// Rich result payload returned inside the contextual `simulateTransaction` response.
pub use self::rpc_transactions::SolanaSimulateTransactionResult;
/// Account-return configuration nested under `simulateTransaction`.
pub use self::rpc_transactions::SolanaSimulationAccountsConfig;
/// Binary encoding accepted for serialized transaction input payloads.
pub use self::rpc_transactions::SolanaTransactionBinaryEncoding;
/// Confirmation state reported for a signature or transaction status.
pub use self::rpc_transactions::SolanaTransactionConfirmationStatus;
/// Encoding accepted by `getTransaction`, including its retained legacy `binary` alias.
pub use self::rpc_transactions::SolanaTransactionEncoding;
/// Transaction version reported by `getTransaction` when the version field is present.
pub use self::rpc_transactions::SolanaTransactionVersion;
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
pub use self::rpc_transactions::SolanaWireField;
/// Open cluster or network descriptor used by HTTP endpoint settings. /// Open cluster or network descriptor used by HTTP endpoint settings.
pub use self::settings::HttpClusterName; pub use self::settings::HttpClusterName;
/// Runtime settings for one role declared by an HTTP endpoint. /// Runtime settings for one role declared by an HTTP endpoint.

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs // file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 9 // version: 10
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -243,3 +243,30 @@ fn public_pre_006_remaining_cluster_wrappers_are_available_from_crate_root() {
let request = ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::default(); let request = ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::default();
assert!(matches!(request, ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::None))); assert!(matches!(request, ksp_onchain_transport_lib::SolanaLeaderScheduleRequest::CurrentEpoch(std::option::Option::None)));
} }
#[test]
fn public_transaction_pre_002_shared_types_are_constructible_from_crate_root() {
let get_transaction = 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!(get_transaction.max_supported_transaction_version(), std::option::Option::Some(0));
let send = ksp_onchain_transport_lib::SolanaSendTransactionConfig::new(
std::option::Option::Some(false),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionBinaryEncoding::Base64),
std::option::Option::Some(3),
std::option::Option::None,
);
assert_eq!(send.max_retries(), std::option::Option::Some(3));
let pagination = ksp_onchain_transport_lib::SolanaSignaturesForAddressConfig::new(
std::option::Option::Some("before".to_owned()),
std::option::Option::None,
std::option::Option::Some(10),
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
std::option::Option::None,
);
assert_eq!(pagination.limit(), std::option::Option::Some(10));
assert!(ksp_onchain_transport_lib::SolanaWireField::<u64>::Omitted.is_omitted());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 6 // version: 7
//! Release-level completeness canaries for the `0.2.1` HTTP foundation contract. //! Release-level completeness canaries for the `0.2.1` HTTP foundation contract.
@@ -194,3 +194,50 @@ fn release_pre_007_v0_2_2_exact_set_is_complete_without_advancing_future_release
assert_eq!(future_transactions, 11); assert_eq!(future_transactions, 11);
assert_eq!(future_blocks_economics, 15); assert_eq!(future_blocks_economics, 15);
} }
#[test]
fn release_v0_2_3_descriptor_set_and_safety_classes_are_exact_before_wrapper_completion() {
let mut expected = std::vec![
"getFeeForMessage",
"getLatestBlockhash",
"getRecentPrioritizationFees",
"getSignaturesForAddress",
"getSignatureStatuses",
"getTransaction",
"getTransactionCount",
"isBlockhashValid",
"requestAirdrop",
"sendTransaction",
"simulateTransaction",
];
let mut actual = std::vec::Vec::new();
let mut reads = 0_usize;
let mut writes = 0_usize;
let mut simulations = 0_usize;
for descriptor in ksp_onchain_transport_lib::current_http_rpc_methods() {
if descriptor.coverage_release() != ksp_onchain_transport_lib::HttpRpcCoverageRelease::V0_2_3 {
continue;
}
actual.push(descriptor.method());
match descriptor.operation_kind() {
ksp_onchain_transport_lib::RpcOperationKind::Read => {
reads += 1;
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
},
ksp_onchain_transport_lib::RpcOperationKind::WriteSubmission => {
writes += 1;
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NeverAfterDispatch);
},
ksp_onchain_transport_lib::RpcOperationKind::Simulation => {
simulations += 1;
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::RetrySafe);
},
}
}
actual.sort_unstable();
expected.sort_unstable();
assert_eq!(actual, expected);
assert_eq!(reads, 8);
assert_eq!(writes, 2);
assert_eq!(simulations, 1);
}

View File

@@ -0,0 +1,286 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
// version: 1
#[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<u64>,
}
let omitted = serde_json::from_value::<Holder>(serde_json::json!({})).expect("omitted fixture must decode");
let null = serde_json::from_value::<Holder>(serde_json::json!({"field": null})).expect("null fixture must decode");
let value = serde_json::from_value::<Holder>(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_eq!(status.search_transaction_history(), std::option::Option::Some(false));
assert_eq!(status.to_json_value(), serde_json::json!({"searchTransactionHistory":false}));
let airdrop = crate::SolanaRequestAirdropConfig::new(
std::option::Option::Some("recent-blockhash".to_owned()),
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
);
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"}));
}
#[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::<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(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::<serde_json::Value>(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::<std::vec::Vec<serde_json::Value>>(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::<std::vec::Vec<serde_json::Value>>(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::<std::vec::Vec<serde_json::Value>>(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::<std::vec::Vec<serde_json::Value>>(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::<std::vec::Vec<serde_json::Value>>(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::<serde_json::Value>(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());
}

271
deltas/0.2.3/pre.002.md Normal file
View File

@@ -0,0 +1,271 @@
<!-- file: deltas/0.2.3/pre.002.md -->
<!-- version: 1 -->
# Delta `0.2.3-pre.002` — primitives Transaction et wire partagé
## Base requise
Livraison précédente :
```text
0.2.3-pre.001
workspace.package.version = "0.2.3-pre.1"
```
Le plan canonique de départ est `docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md` version 2, avec l'alignement Markdown corrigé par
l'opérateur avant cette tranche.
## Objectif
Installer les primitives, configurations et résultats partagés nécessaires aux 11 wrappers Transactions `0.2.3`, ainsi que des fixtures wire
locales déterministes, sans encore implémenter les quatre premiers wrappers Read prévus pour `pre.003` et sans déclarer la couverture typed
`0.2.3` complète.
Cette tranche doit préserver les unions/nullable/omissions du wire Transaction tout en restant une couche Transport : aucun décodage Program,
aucune construction/signature de transaction et aucun client RPC Solana parallèle ne sont introduits.
## Version Cargo
Conformément à `VER-ID-009`, la nouvelle prerelease synchronise le signal technique :
```text
0.2.3-pre.1 -> 0.2.3-pre.2
```
Aucune dépendance ou feature Cargo n'est ajoutée.
## Implémentation
### Encodings et champs wire
Le nouveau module `rpc_transactions` ajoute :
```text
SolanaTransactionBinaryEncoding
SolanaTransactionEncoding
SolanaWireField<T>
```
`SolanaTransactionBinaryEncoding` borne les entrées sérialisées `sendTransaction`/`simulateTransaction` à `base58 | base64` sans décoder les
bytes. `SolanaTransactionEncoding` conserve la surface plus large de `getTransaction` : `binary` legacy, `base58`, `base64`, `json` et
`jsonParsed`.
`SolanaWireField<T>` matérialise explicitement les trois états qui comptent sur les réponses Transaction évolutives :
```text
Omitted
Null
Value(T)
```
Cette primitive évite de fusionner un champ absent avec un champ explicitement `null`, notamment pour `getTransaction` et
`simulateTransaction`.
### Configurations partagées
Les configurations suivantes sont ajoutées :
```text
SolanaGetTransactionConfig
SolanaSignaturesForAddressConfig
SolanaSignatureStatusesConfig
SolanaRequestAirdropConfig
SolanaSendTransactionConfig
SolanaSimulationAccountsConfig
SolanaSimulateTransactionConfig
```
Elles sérialisent uniquement les options explicitement fournies. `SolanaSendTransactionConfig::max_retries` reste nommé et documenté comme une
limite de retransmission **côté noeud RPC** et ne modifie jamais la retry policy HTTP KSP.
Les validations propres aux wrappers restent volontairement dans leurs tranches d'exécution : limites `128 / 1000 / 256`, encodings Account
interdits pour le retour de simulation et incompatibilité `sigVerify + replaceRecentBlockhash` ne sont pas transformés ici en une seconde policy
parallèle.
### Résultats et DTOs Transaction
Les résultats communs suivants sont ajoutés :
```text
SolanaLatestBlockhash
SolanaPrioritizationFee
SolanaTransactionConfirmationStatus
SolanaSignatureInfo
SolanaSignatureStatus
SolanaEncodedTransaction
SolanaTransactionVersion
SolanaConfirmedTransaction
SolanaSimulateTransactionResult
```
`SolanaEncodedTransaction` conserve séparément :
```text
chaîne binaire legacy
[data, "base58" | "base64"]
objet JSON / jsonParsed
```
Aucun `base64`, `bs58` ou `wincode` n'est donc requis. Les objets Transaction/message riches restent lossless via `serde_json::Value` aux
frontières où Transport ne possède pas de modèle métier.
`SolanaConfirmedTransaction` conserve `meta`, `version` et `transactionIndex` avec la sémantique `Omitted | Null | Value`. Pendant
l'implémentation de cette tranche, le recoupement direct avec Agave `v4.2.1` a confirmé que `EncodedConfirmedTransactionWithStatusMeta` expose
également `transactionIndex: Option<u32>` au top-level de `getTransaction`. Le plan `010` passe donc en version 3 pour enregistrer cette précision
wire additive ; il ne s'agit pas d'un changement de périmètre.
`SolanaSimulateTransactionResult` conserve les champs Agave courants utiles, y compris les comptes positionnels nullable, unités consommées,
loaded account data size, return data, inner instructions, replacement blockhash, fee, balances, token balances et loaded addresses. Les
sous-structures Program/transaction riches restent volontairement opaques lorsqu'un modèle Transport étroit n'apporte pas de valeur.
### Validation de wire locale
Les décodeurs partagés :
- utilisent le domaine d'erreur Transport existant pour les shapes invalides ;
- rejettent un `getLatestBlockhash.blockhash` vide sans tenter de le décoder ;
- rejettent les labels de confirmation inconnus ;
- rejettent les tuples transaction binaires mal formés ou utilisant un encoding non binaire ;
- bornent le champ `version` de `getTransaction` à `legacy | u8` conformément au wire actuel ;
- préservent les `null` positionnels des comptes de simulation.
Ils ne désérialisent ni message ni transaction Solana.
## Fixtures déterministes ajoutées
```text
crates/ksp-onchain-transport-lib/fixtures/http/latest_blockhash.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/prioritization_fee.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/signature_info.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/signature_status.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/encoded_transaction.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/confirmed_transaction.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/simulate_transaction_result.v4_2_1.json
```
Elles couvrent notamment :
- latest blockhash ;
- ordre de samples de prioritization fees ;
- `transactionIndex` présent et absent sur les signatures ;
- `null` positionnel pour les statuses ;
- status legacy encore présent ;
- transaction legacy, tuple base64 et JSON ;
- `meta/version/transactionIndex` présents, `null` ou omis ;
- résultat de simulation riche avec compte positionnel `null` et champs optionnels actuels.
## Tests ajoutés
`unit_tests/rpc_transactions.rs` ajoute **18 tests déterministes** pour les enums/configs/décodeurs/fixtures de cette tranche.
Les tests de surface ajoutent aussi :
- un test public de construction/réexport des primitives Transaction depuis la racine de crate ;
- une canarie de registre qui confirme l'ensemble exact des 11 descriptors `0.2.3` et leur répartition `8 Read / 2 WriteSubmission / 1 Simulation`
sans prétendre que les 11 wrappers typed existent déjà.
La canarie typed-complete `0.2.3 exact == 11` reste donc volontairement fermée jusqu'aux tranches de wrappers.
## Fichiers ajoutés
```text
crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
crates/ksp-onchain-transport-lib/fixtures/http/latest_blockhash.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/prioritization_fee.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/signature_info.v4_2_1.json
crates/ksp-onchain-transport-lib/fixtures/http/signature_status.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/encoded_transaction.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/confirmed_transaction.variants.json
crates/ksp-onchain-transport-lib/fixtures/http/simulate_transaction_result.v4_2_1.json
deltas/0.2.3/pre.002.md
```
## Fichiers modifiés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md
```
## Fichiers supprimés
Aucun.
## Fichiers volontairement inchangés
```text
CHANGELOG.md
ROADMAP.md
crates/ksp-onchain-transport-lib/Cargo.toml
crates/ksp-onchain-transport-lib/README.md
crates/ksp-onchain-transport-lib/USAGE.md
crates/ksp-onchain-transport-lib/src/executor.rs
crates/ksp-onchain-transport-lib/src/rpc_method.rs
crates/ksp-config-lib/**
config/**
```
Aucun ancien delta n'est réécrit. Aucun wrapper `0.2.4` n'est avancé.
## Validations exécutées
- reconstruction de la base `0.2.3-pre.001` puis remplacement du plan `010` par la version 2 corrigée fournie par l'opérateur ;
- contrôle différentiel de la tranche contre cette base corrigée ;
- recoupement ciblé des unions/configs/résultats avec les sources primaires Agave `v4.2.1` déjà normatives pour le plan ;
- confirmation additive de `getTransaction.transactionIndex: Option<u32>` dans `EncodedConfirmedTransactionWithStatusMeta` ;
- parse JSON local des sept nouvelles fixtures ;
- contrôle statique des réexports crate-root des nouveaux types publics ;
- contrôle statique de l'absence de nouveau client HTTP, de `reqwest` direct dans le module Transaction, de boucle de retry locale et de wrapper
Transaction exécuté prématurément ;
- contrôle statique de l'absence de nouvelle dépendance/feature Cargo ;
- contrôle statique de l'absence de `unwrap`, `expect`, `panic!` et `?` dans la nouvelle source de production ;
- contrôle statique des lignes Rust modifiées à `<= 160` colonnes ;
- contrôle que `CHANGELOG.md` reste inchangé.
## Validations non exécutées
Le sandbox ne fournit pas `cargo` ni `rustfmt`. Les commandes suivantes ne sont donc pas déclarées comme réussies et doivent être exécutées sur
le checkout de développement avant commit :
```bash
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
```
Aucun `cargo tree` supplémentaire n'est requis par cette tranche puisqu'aucune dépendance ni feature n'a changé. Les vues de dépendances du prompt
restent obligatoires à la clôture de `0.2.3`.
Le commit attendu après application et validations suit `VER-GIT-001` :
```text
v0.2.3-pre.002
```
## Décisions prises
- conserver les payloads message/transaction sérialisés comme chaînes opaques tant qu'aucun invariant KSP n'exige leur décodage ;
- distinguer l'encoding binaire d'entrée de la surface d'encoding plus large de `getTransaction` ;
- représenter explicitement `absent / null / présent` lorsque le wire courant le nécessite ;
- conserver `serde_json::Value` seulement aux frontières Transaction/Program riches et évolutives, plutôt que dupliquer tout
`solana-transaction-status-client-types` ;
- réutiliser `SolanaContextConfig`, `SolanaAccountEncoding`, `SolanaAccount` et les erreurs Transport existantes ;
- ne pas ajouter les validations de wrapper avant leur tranche propriétaire ;
- ajouter `getTransaction.transactionIndex` au plan et au DTO après confirmation Agave `v4.2.1`.
## Questions ouvertes
Aucune question bloquante pour `pre.003`.
Le formatage canonique final doit être produit par `cargo fmt --all`/`rustfmt` lors de l'application locale, puisque ces binaires ne sont pas
disponibles dans le sandbox d'échange.
## Suite
`0.2.3-pre.003` : implémenter `getFeeForMessage`, `getLatestBlockhash`, `getTransactionCount` et `isBlockhashValid` via le chemin
`descriptor -> execute_standard_rpc`, avec requêtes exactes, résultats contextuels/nullables, erreurs RPC et fixtures HTTP locales.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md --> <!-- file: docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md -->
<!-- version: 2 --> <!-- version: 3 -->
# Plan `0.2.3` — HTTP Transactions # Plan `0.2.3` — HTTP Transactions
@@ -314,13 +314,18 @@ Cette API legacy doit être nommée/annotée de manière à ne pas sembler être
Le résultat confirmé garde un top-level typé : Le résultat confirmé garde un top-level typé :
```text ```text
slot u64 slot u64
blockTime Option<i64> blockTime Option<i64>
transaction EncodedTransaction transaction EncodedTransaction
meta Option<transaction-meta wire> meta absent | null | transaction-meta wire
version legacy | number version absent | null | legacy | number
transactionIndex absent | null | u32
``` ```
Le recoupement d'implémentation `pre.002` avec `transaction-status-client-types` Agave `v4.2.1` a confirmé que
`EncodedConfirmedTransactionWithStatusMeta` expose également `transactionIndex: Option<u32>` au top-level de `getTransaction`. Cette extension
optionnelle est donc préservée comme celle de `getSignaturesForAddress`, sans être rendue obligatoire pour les providers plus anciens.
`EncodedTransaction` doit préserver les différentes formes réellement sérialisées : `EncodedTransaction` doit préserver les différentes formes réellement sérialisées :
```text ```text
@@ -335,6 +340,10 @@ sont riches, évolutives ou dépendantes de `jsonParsed` peuvent rester lossless
Le meta doit au minimum respecter la distinction **absent/null/présent** imposée par le wire et ne jamais inventer des listes ou zéros lorsque le Le meta doit au minimum respecter la distinction **absent/null/présent** imposée par le wire et ne jamais inventer des listes ou zéros lorsque le
provider omet une donnée optionnelle. provider omet une donnée optionnelle.
`pre.002` matérialise cette distinction avec une primitive générique `SolanaWireField<T>` limitée au wire Transport : `Omitted`, `Null`, `Value(T)`.
Elle est utilisée pour les champs Transaction/Simulation où l'omission et `null` doivent rester distinguables ; elle ne transforme pas ce mécanisme
en modèle métier et ne remplace pas `Option<T>` lorsque les deux états ont le même sens contractuel.
### `getRecentPrioritizationFees` ### `getRecentPrioritizationFees`
Le paramètre optionnel est un tableau d'adresses. La documentation actuelle fixe un maximum de **128** et précise que, lorsqu'il est fourni, les Le paramètre optionnel est un tableau d'adresses. La documentation actuelle fixe un maximum de **128** et précise que, lorsqu'il est fourni, les
@@ -634,3 +643,18 @@ Aucune commande n'est déclarée réussie sans exécution réelle ou preuve opé
- découpage `pre.002``pre.008` défini. - découpage `pre.002``pre.008` défini.
La tranche suivante peut commencer par les primitives wire communes sans réouvrir le scope de release. La tranche suivante peut commencer par les primitives wire communes sans réouvrir le scope de release.
## Résultat de `pre.002`
`pre.002` installe les primitives/configurations/résultats partagés sans déclarer un wrapper Transaction comme terminé :
- encodings Transaction séparés entre entrées binaires `base58/base64` et surface `getTransaction` incluant les formes JSON/legacy ;
- configs modernes de `getTransaction`, pagination signatures, statuses, airdrop, send et simulation ;
- DTOs `latestBlockhash`, prioritization fee, signature info/status, transaction encodée/versionnée et résultat de simulation ;
- primitive `SolanaWireField<T>` pour préserver explicitement `omitted/null/value` ;
- `transactionIndex` préservé sur `getSignaturesForAddress` et sur le top-level `getTransaction` courant ;
- transaction/meta/return data/inner instructions/token balances riches conservés losslessly aux frontières prévues sans décodage Program ;
- fixtures communes déterministes pour les variantes legacy/binary/JSON, version/meta/index et simulation riche ;
- aucune nouvelle dépendance Cargo et aucun wrapper `0.2.3` activé prématurément.
La tranche suivante reste `pre.003` : `getFeeForMessage`, `getLatestBlockhash`, `getTransactionCount` et `isBlockhashValid`.