v0.2.9-pre.007
This commit is contained in:
@@ -6,7 +6,7 @@ resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.9-pre.6"
|
||||
version = "0.2.9-pre.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 39
|
||||
// version: 40
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -36,8 +36,10 @@
|
||||
//! `0.2.9-pre.003` adds bounded TLS/WebPKI connection establishment, generic redacted ASCII request metadata and the seven standard Yellowstone unary RPCs
|
||||
//! through KSP-owned DTOs.
|
||||
//! `0.2.9-pre.004` materializes the provider-neutral standard `SubscribeRequest` foundation: all seven named filter maps, global filter-name bounds/uniqueness,
|
||||
//! commitment, ordered account-data slices, ping and `from_slot`. Family-specific account/slot filters land in `pre.005`; transaction/block filters remain staged for `pre.007–008`.
|
||||
//! `0.2.9-pre.006` normalizes the five unambiguously HTTP-owned private implementation modules with an `http_` prefix while preserving shared `rpc_*`, JSON-RPC, error and constants modules.
|
||||
//! commitment, ordered account-data slices, ping and `from_slot`. Family-specific account/slot filters land in `pre.005`; transaction/block filters remain
|
||||
//! staged for `pre.007–008`.
|
||||
//! `0.2.9-pre.006` normalizes the five unambiguously HTTP-owned private implementation modules with an `http_` prefix while preserving shared `rpc_*`,
|
||||
//! JSON-RPC, error and constants modules.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
@@ -71,14 +73,6 @@ mod ws_settings;
|
||||
mod ws_subscription;
|
||||
mod ws_transactions;
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||
pub use self::http_client::HttpEndpointAvailability;
|
||||
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||
pub use self::http_client::HttpEndpointClient;
|
||||
/// Safe routing snapshot for one configured endpoint role.
|
||||
pub use self::http_client::HttpEndpointRoleSnapshot;
|
||||
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||
pub use self::http_client::HttpEndpointSnapshot;
|
||||
/// Error code used when no logical endpoint can satisfy a request.
|
||||
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
|
||||
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
|
||||
@@ -135,24 +129,46 @@ pub use self::grpc_settings::YellowstoneGrpcReconnectSettings;
|
||||
pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
|
||||
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
|
||||
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
|
||||
/// Typed account payload carried by one standard Yellowstone account update.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountInfo;
|
||||
/// One validated standard Yellowstone account predicate.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountFilterPredicate;
|
||||
/// Standard Yellowstone account-update projection owned by KSP.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountUpdate;
|
||||
/// Typed account payload carried by one standard Yellowstone account update.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountInfo;
|
||||
/// Lamport comparison used by standard Yellowstone account filters.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountLamportsFilter;
|
||||
/// Validated standard Yellowstone account memcmp predicate.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountMemcmp;
|
||||
/// Encoding selected by one Yellowstone account memcmp predicate.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountMemcmpEncoding;
|
||||
/// Lamport comparison used by standard Yellowstone account filters.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountLamportsFilter;
|
||||
/// Standard Yellowstone account-update projection owned by KSP.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountUpdate;
|
||||
/// One standard Yellowstone account-data slice.
|
||||
pub use self::grpc_subscribe::YellowstoneAccountsDataSlice;
|
||||
/// Hash algorithm carried by a standard Yellowstone Cuckoo filter.
|
||||
pub use self::grpc_subscribe::YellowstoneCuckooHashAlgorithm;
|
||||
/// One compiled instruction from the Yellowstone Solana-storage transaction wire.
|
||||
pub use self::grpc_subscribe::YellowstoneCompiledInstruction;
|
||||
/// Wire-preserving KSP representation of a standard Yellowstone Cuckoo filter.
|
||||
pub use self::grpc_subscribe::YellowstoneCuckooFilter;
|
||||
/// Hash algorithm carried by a standard Yellowstone Cuckoo filter.
|
||||
pub use self::grpc_subscribe::YellowstoneCuckooHashAlgorithm;
|
||||
/// Fixed-width 32-byte hash from the Yellowstone Solana-storage transaction wire.
|
||||
pub use self::grpc_subscribe::YellowstoneHashBytes;
|
||||
/// One inner instruction from Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneInnerInstruction;
|
||||
/// One indexed inner-instruction group from Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneInnerInstructions;
|
||||
/// One address-table lookup from a Yellowstone transaction message.
|
||||
pub use self::grpc_subscribe::YellowstoneMessageAddressTableLookup;
|
||||
/// Return-data payload from Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneReturnData;
|
||||
/// One reward entry from Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneReward;
|
||||
/// Reward classification from the Yellowstone Solana-storage wire.
|
||||
pub use self::grpc_subscribe::YellowstoneRewardType;
|
||||
/// Current standard Yellowstone slot status.
|
||||
pub use self::grpc_subscribe::YellowstoneSlotStatus;
|
||||
/// Standard Yellowstone slot-update projection owned by KSP.
|
||||
pub use self::grpc_subscribe::YellowstoneSlotUpdate;
|
||||
/// Solana transaction body carried by Yellowstone storage protobuf messages.
|
||||
pub use self::grpc_subscribe::YellowstoneStoredTransaction;
|
||||
/// Complete account-family filter group for standard Yellowstone Subscribe.
|
||||
pub use self::grpc_subscribe::YellowstoneSubscribeAccountFilter;
|
||||
/// Block-family filter-group shell for standard Yellowstone Subscribe.
|
||||
@@ -169,16 +185,36 @@ pub use self::grpc_subscribe::YellowstoneSubscribePing;
|
||||
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
|
||||
/// Complete slot-family filter group for standard Yellowstone Subscribe.
|
||||
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
|
||||
/// Current standard Yellowstone slot status.
|
||||
pub use self::grpc_subscribe::YellowstoneSlotStatus;
|
||||
/// Standard Yellowstone slot-update projection owned by KSP.
|
||||
pub use self::grpc_subscribe::YellowstoneSlotUpdate;
|
||||
/// Fixed-width transaction signature attached to Yellowstone account updates when available.
|
||||
/// Complete transaction-family filter shared by transactions and transaction-status maps.
|
||||
pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter;
|
||||
/// Optional token-account owner expansion for current Yellowstone transaction filters.
|
||||
pub use self::grpc_subscribe::YellowstoneTokenAccountExpansion;
|
||||
/// One pre/post token balance from Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneTokenBalance;
|
||||
/// Optional Transaction V1 inline budget configuration from Yellowstone Solana-storage.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionConfig;
|
||||
/// Opaque runtime transaction error bytes from Yellowstone Solana-storage.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionError;
|
||||
/// Complete transaction info carried by Yellowstone transaction and block updates.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionInfo;
|
||||
/// Complete current Yellowstone transaction message.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionMessage;
|
||||
/// Solana transaction message header from Yellowstone Solana-storage.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionMessageHeader;
|
||||
/// Fixed-width transaction signature attached to Yellowstone updates.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionSignature;
|
||||
/// Validated base58 transaction-signature selector for Yellowstone transaction filters.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionSignatureSelector;
|
||||
/// Complete Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionStatusMeta;
|
||||
/// Lightweight Yellowstone transaction-status update.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionStatusUpdate;
|
||||
/// Full Yellowstone transaction update.
|
||||
pub use self::grpc_subscribe::YellowstoneTransactionUpdate;
|
||||
/// UI token amount from Yellowstone transaction status metadata.
|
||||
pub use self::grpc_subscribe::YellowstoneUiTokenAmount;
|
||||
/// Timestamp attached to standard Yellowstone update envelopes.
|
||||
pub use self::grpc_subscribe::YellowstoneUpdateTimestamp;
|
||||
/// Transaction-family filter-group shell shared by transactions and transaction-status maps.
|
||||
pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter;
|
||||
/// Standard Solana Yellowstone unary facade over one KSP-owned physical gRPC channel.
|
||||
pub use self::grpc_unary::SolanaYellowstoneGrpcUnaryClient;
|
||||
/// Block height returned by the standard Yellowstone unary surface.
|
||||
@@ -195,20 +231,14 @@ pub use self::grpc_unary::YellowstoneReplayInfo;
|
||||
pub use self::grpc_unary::YellowstoneSlot;
|
||||
/// Bounded endpoint version returned by the standard Yellowstone unary surface.
|
||||
pub use self::grpc_unary::YellowstoneVersionInfo;
|
||||
/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
|
||||
pub use self::json_rpc::JsonRpcErrorObject;
|
||||
/// Validated JSON-RPC 2.0 error response.
|
||||
pub use self::json_rpc::JsonRpcErrorResponse;
|
||||
/// JSON-RPC 2.0 HTTP request envelope emitted by KSP.
|
||||
pub use self::json_rpc::JsonRpcRequest;
|
||||
/// Validated JSON-RPC 2.0 HTTP response.
|
||||
pub use self::json_rpc::JsonRpcResponse;
|
||||
/// Validated JSON-RPC 2.0 success response.
|
||||
pub use self::json_rpc::JsonRpcSuccessResponse;
|
||||
/// Parses and validates a JSON-RPC HTTP response from UTF-8 JSON text.
|
||||
pub use self::json_rpc::parse_json_rpc_response_text;
|
||||
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
||||
pub use self::json_rpc::parse_json_rpc_response_value;
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||
pub use self::http_client::HttpEndpointAvailability;
|
||||
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||
pub use self::http_client::HttpEndpointClient;
|
||||
/// Safe routing snapshot for one configured endpoint role.
|
||||
pub use self::http_client::HttpEndpointRoleSnapshot;
|
||||
/// Safe metadata snapshot for one logical HTTP endpoint.
|
||||
pub use self::http_client::HttpEndpointSnapshot;
|
||||
/// Result of one logical endpoint selection.
|
||||
pub use self::http_pool::HttpEndpointSelection;
|
||||
/// Runtime admission permit for one HTTP request.
|
||||
@@ -225,6 +255,40 @@ pub use self::http_resilience::HttpRetryCause;
|
||||
pub use self::http_resilience::HttpRetryDecision;
|
||||
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
|
||||
pub use self::http_resilience::evaluate_transport_retry;
|
||||
/// Open cluster or network descriptor used by HTTP endpoint settings.
|
||||
pub use self::http_settings::HttpClusterName;
|
||||
/// Runtime settings for one role declared by an HTTP endpoint.
|
||||
pub use self::http_settings::HttpEndpointRoleSettings;
|
||||
/// Runtime settings for one named Solana HTTP endpoint.
|
||||
pub use self::http_settings::HttpEndpointSettings;
|
||||
/// Runtime HTTP endpoint URL with redacted diagnostics.
|
||||
pub use self::http_settings::HttpEndpointUrl;
|
||||
/// Open provider descriptor used by HTTP endpoint settings.
|
||||
pub use self::http_settings::HttpProviderName;
|
||||
/// Open request-kind descriptor used by logical endpoint capabilities.
|
||||
pub use self::http_settings::HttpRequestKind;
|
||||
/// Bounded retry settings owned by the HTTP transport runtime.
|
||||
pub use self::http_settings::HttpRetrySettings;
|
||||
/// Local limits attached to one logical HTTP endpoint role.
|
||||
pub use self::http_settings::HttpRoleLimits;
|
||||
/// Open logical endpoint role descriptor.
|
||||
pub use self::http_settings::HttpRoleName;
|
||||
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
||||
pub use self::http_settings::HttpTransportSettings;
|
||||
/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
|
||||
pub use self::json_rpc::JsonRpcErrorObject;
|
||||
/// Validated JSON-RPC 2.0 error response.
|
||||
pub use self::json_rpc::JsonRpcErrorResponse;
|
||||
/// JSON-RPC 2.0 HTTP request envelope emitted by KSP.
|
||||
pub use self::json_rpc::JsonRpcRequest;
|
||||
/// Validated JSON-RPC 2.0 HTTP response.
|
||||
pub use self::json_rpc::JsonRpcResponse;
|
||||
/// Validated JSON-RPC 2.0 success response.
|
||||
pub use self::json_rpc::JsonRpcSuccessResponse;
|
||||
/// Parses and validates a JSON-RPC HTTP response from UTF-8 JSON text.
|
||||
pub use self::json_rpc::parse_json_rpc_response_text;
|
||||
/// Validates a decoded JSON value as one JSON-RPC HTTP response.
|
||||
pub use self::json_rpc::parse_json_rpc_response_value;
|
||||
/// Typed transport-level Solana account without Program/SPL decoding.
|
||||
pub use self::rpc_accounts::SolanaAccount;
|
||||
/// Address and lamport balance returned by `getLargestAccounts`.
|
||||
@@ -397,26 +461,6 @@ pub use self::rpc_transactions::SolanaTransactionEncoding;
|
||||
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.
|
||||
pub use self::http_settings::HttpClusterName;
|
||||
/// Runtime settings for one role declared by an HTTP endpoint.
|
||||
pub use self::http_settings::HttpEndpointRoleSettings;
|
||||
/// Runtime settings for one named Solana HTTP endpoint.
|
||||
pub use self::http_settings::HttpEndpointSettings;
|
||||
/// Runtime HTTP endpoint URL with redacted diagnostics.
|
||||
pub use self::http_settings::HttpEndpointUrl;
|
||||
/// Open provider descriptor used by HTTP endpoint settings.
|
||||
pub use self::http_settings::HttpProviderName;
|
||||
/// Open request-kind descriptor used by logical endpoint capabilities.
|
||||
pub use self::http_settings::HttpRequestKind;
|
||||
/// Bounded retry settings owned by the HTTP transport runtime.
|
||||
pub use self::http_settings::HttpRetrySettings;
|
||||
/// Local limits attached to one logical HTTP endpoint role.
|
||||
pub use self::http_settings::HttpRoleLimits;
|
||||
/// Open logical endpoint role descriptor.
|
||||
pub use self::http_settings::HttpRoleName;
|
||||
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
||||
pub use self::http_settings::HttpTransportSettings;
|
||||
/// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method.
|
||||
pub use self::ws_accounts::SolanaAccountSubscribeConfig;
|
||||
/// One `programNotification` payload preserving contextual and non-contextual upstream forms.
|
||||
@@ -510,12 +554,12 @@ pub(crate) use self::http_resilience::HttpConcurrencyPermit;
|
||||
pub(crate) use self::http_resilience::HttpRoleRuntime;
|
||||
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
|
||||
pub(crate) use self::http_resilience::RoleAdmissionAttempt;
|
||||
/// Validates endpoint settings.
|
||||
pub(crate) use self::http_settings::validate_endpoint_settings;
|
||||
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::decode_wire_json;
|
||||
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||
/// Validates endpoint settings.
|
||||
pub(crate) use self::http_settings::validate_endpoint_settings;
|
||||
/// Crate-internal command surface shared by the physical session and typed subscription handle.
|
||||
pub(crate) use self::ws_session::WsSessionCommand;
|
||||
/// Crate-internal notification dispatch result.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 43
|
||||
// version: 44
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -896,3 +896,32 @@ fn public_v0_2_9_pre_005_yellowstone_accounts_slots_contract_is_available_from_c
|
||||
let _lamports = ksp_onchain_transport_lib::YellowstoneAccountLamportsFilter::Gt(10);
|
||||
let _encoding = ksp_onchain_transport_lib::YellowstoneAccountMemcmpEncoding::Bytes;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_v0_2_9_pre_007_yellowstone_transactions_contract_is_available_from_crate_root() {
|
||||
let mut filter = ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new();
|
||||
filter.set_vote(std::option::Option::Some(false));
|
||||
filter.set_failed(std::option::Option::Some(true));
|
||||
let selector = ksp_onchain_transport_lib::YellowstoneTransactionSignatureSelector::new("1".repeat(64)).expect("signature selector must validate");
|
||||
filter.set_signature(std::option::Option::Some(selector));
|
||||
assert!(filter.push_account_include(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])).is_ok());
|
||||
assert!(filter.push_account_exclude(ksp_core_lib::Pubkey::new_from_array([2_u8; 32])).is_ok());
|
||||
assert!(filter.push_account_required(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
|
||||
filter.set_token_accounts(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneTokenAccountExpansion::All));
|
||||
assert_eq!(filter.token_accounts(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneTokenAccountExpansion::All));
|
||||
let _hash = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneHashBytes>();
|
||||
let _instruction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneCompiledInstruction>();
|
||||
let _lookup = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneMessageAddressTableLookup>();
|
||||
let _config = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionConfig>();
|
||||
let _message = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionMessage>();
|
||||
let _stored = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneStoredTransaction>();
|
||||
let _meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusMeta>();
|
||||
let _info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionInfo>();
|
||||
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionUpdate>();
|
||||
let _status = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate>();
|
||||
let _error = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionError>();
|
||||
let _inner = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneInnerInstructions>();
|
||||
let _token = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTokenBalance>();
|
||||
let _return_data = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneReturnData>();
|
||||
let _reward = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneReward>();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 37
|
||||
// version: 38
|
||||
|
||||
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
||||
|
||||
@@ -1071,7 +1071,19 @@ fn release_v0_2_9_pre_003_adds_tls_metadata_and_exactly_seven_standard_unary_met
|
||||
fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract() {
|
||||
let source = include_str!("../src/grpc_subscribe.rs");
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
for wire_field in ["accounts:", "slots:", "transactions:", "transactions_status:", "blocks:", "blocks_meta:", "entry:", "commitment:", "accounts_data_slice:", "ping:", "from_slot:"] {
|
||||
for wire_field in [
|
||||
"accounts:",
|
||||
"slots:",
|
||||
"transactions:",
|
||||
"transactions_status:",
|
||||
"blocks:",
|
||||
"blocks_meta:",
|
||||
"entry:",
|
||||
"commitment:",
|
||||
"accounts_data_slice:",
|
||||
"ping:",
|
||||
"from_slot:",
|
||||
] {
|
||||
assert!(source.contains(wire_field), "missing standard Yellowstone SubscribeRequest field: {wire_field}");
|
||||
}
|
||||
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT"));
|
||||
@@ -1134,7 +1146,6 @@ fn release_v0_2_9_pre_005_completes_standard_accounts_and_slots_without_advancin
|
||||
assert!(source.contains("MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES"));
|
||||
assert!(source.contains("#[cfg(test)]\nfn decode_account_update"));
|
||||
assert!(source.contains("#[cfg(test)]\nfn decode_slot_update"));
|
||||
assert!(!source.contains("YellowstoneTransactionUpdate"));
|
||||
assert!(!source.contains("YellowstoneBlockUpdate"));
|
||||
assert!(!source.contains("SubscribeDeshred"));
|
||||
assert!(!source.contains("PublicNode"));
|
||||
@@ -1168,3 +1179,45 @@ fn release_v0_2_9_pre_006_namespaces_unambiguously_http_owned_private_modules()
|
||||
assert!(crate_root.contains(shared), "shared/protocol module was incorrectly HTTP-prefixed: {shared}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_v0_2_9_pre_007_completes_standard_transactions_without_advancing_blocks_or_bidi() {
|
||||
let source = include_str!("../src/grpc_subscribe.rs");
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
for required in [
|
||||
"YellowstoneSubscribeTransactionFilter",
|
||||
"YellowstoneTransactionSignatureSelector",
|
||||
"YellowstoneTokenAccountExpansion",
|
||||
"account_include",
|
||||
"account_exclude",
|
||||
"account_required",
|
||||
"cuckoo_account_include",
|
||||
"token_accounts",
|
||||
"YellowstoneTransactionUpdate",
|
||||
"YellowstoneTransactionStatusUpdate",
|
||||
"YellowstoneTransactionStatusMeta",
|
||||
"YellowstoneTransactionMessage",
|
||||
"YellowstoneMessageAddressTableLookup",
|
||||
"YellowstoneTransactionConfig",
|
||||
"compute_units_consumed",
|
||||
"cost_units",
|
||||
"inner_instructions_none",
|
||||
"log_messages_none",
|
||||
"return_data_none",
|
||||
"decode_transaction_update",
|
||||
"decode_transaction_status_update",
|
||||
] {
|
||||
assert!(source.contains(required), "missing pre.007 Transactions contract token: {required}");
|
||||
}
|
||||
assert!(source.contains("base58_decoded_length"));
|
||||
assert!(source.contains("YELLOWSTONE_TRANSACTION_SIGNATURE_WIRE_LENGTH_BYTES"));
|
||||
assert!(!source.contains("YellowstoneBlockUpdate"));
|
||||
assert!(!source.contains("fn decode_block_update"));
|
||||
assert!(!source.contains("SubscribeDeshred"));
|
||||
assert!(!source.contains("PublicNode"));
|
||||
assert!(!crate_root.contains("pub use tonic"));
|
||||
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
|
||||
let _filter = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
|
||||
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionUpdate>();
|
||||
let _status = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate>();
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs
|
||||
// version: 3
|
||||
|
||||
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
return crate::HttpEndpointSettings::new(
|
||||
"endpoint",
|
||||
enabled,
|
||||
crate::HttpProviderName::new("provider"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(2),
|
||||
std::option::Option::Some(4),
|
||||
std::vec![role],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
|
||||
let snapshot = client.snapshot();
|
||||
let rendered = format!("{snapshot:?} {client:?}");
|
||||
assert_eq!(snapshot.availability(), crate::HttpEndpointAvailability::Available);
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
assert!(!rendered.contains("provider.invalid"));
|
||||
assert!(!rendered.contains("https://"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_endpoint_client_is_visible_but_not_selectable() {
|
||||
let client = crate::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
|
||||
assert_eq!(client.snapshot().availability(), crate::HttpEndpointAvailability::Disabled);
|
||||
assert!(!client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_client_matches_exact_and_wildcard_capabilities() {
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
assert!(client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||
assert!(!client.supports(&crate::HttpRoleName::new("write"), &crate::HttpRequestKind::new("get_balance")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_role_snapshot_exposes_safe_resilience_state() {
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let snapshot = client.snapshot();
|
||||
let role = &snapshot.roles()[0];
|
||||
assert_eq!(role.availability(), crate::HttpEndpointAvailability::Available);
|
||||
assert_eq!(role.in_flight_requests(), std::option::Option::None);
|
||||
assert_eq!(role.cooldown_remaining(), std::option::Option::None);
|
||||
assert_eq!(role.success_count(), 0);
|
||||
assert_eq!(role.failure_count(), 0);
|
||||
assert_eq!(role.rate_limit_count(), 0);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/executor.rs
|
||||
// version: 2
|
||||
|
||||
fn pool_for_url(url: &str, request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(std::time::Duration::from_millis(1)),
|
||||
),
|
||||
);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"fixture",
|
||||
true,
|
||||
crate::HttpProviderName::new("fixture"),
|
||||
crate::HttpClusterName::new("local"),
|
||||
crate::HttpEndpointUrl::parse(url).expect("fixture URL must parse"),
|
||||
std::time::Duration::from_millis(100),
|
||||
request_timeout,
|
||||
std::option::Option::Some(1),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
crate::HttpRetrySettings::new(max_retries, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
);
|
||||
return crate::HttpTransportPool::new(settings).expect("fixture pool must build");
|
||||
}
|
||||
|
||||
fn health_method() -> &'static crate::HttpRpcMethodDescriptor {
|
||||
return crate::find_http_rpc_method("getHealth").expect("getHealth descriptor must exist");
|
||||
}
|
||||
|
||||
fn serve_rate_limit_then_success() -> (std::string::String, std::thread::JoinHandle<usize>) {
|
||||
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 count = 0_usize;
|
||||
while count < 2 {
|
||||
let (mut stream, _) = listener.accept().expect("fixture server must accept request");
|
||||
let _ = read_request(&mut stream);
|
||||
let response = if count == 0 {
|
||||
"HTTP/1.1 429 Too Many Requests\r\nRetry-After: 0\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".to_owned()
|
||||
} else {
|
||||
let body = include_str!("../fixtures/http/get_health.success.json");
|
||||
format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body)
|
||||
};
|
||||
std::io::Write::write_all(&mut stream, response.as_bytes()).expect("fixture response must write");
|
||||
count = count.saturating_add(1);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn serve_timeout() -> (std::string::String, std::thread::JoinHandle<()>) {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("fixture listener must bind");
|
||||
let address = listener.local_addr().expect("fixture listener address must resolve");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("fixture server must accept request");
|
||||
let _ = read_request(&mut stream);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
return;
|
||||
});
|
||||
return (format!("http://{address}"), handle);
|
||||
}
|
||||
|
||||
fn read_request(stream: &mut std::net::TcpStream) -> std::string::String {
|
||||
let mut bytes = std::vec::Vec::new();
|
||||
let mut buffer = [0_u8; 1024];
|
||||
loop {
|
||||
let count = std::io::Read::read(stream, &mut buffer).expect("fixture request must read");
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(&buffer[..count]);
|
||||
if request_complete(bytes.as_slice()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return std::string::String::from_utf8(bytes).expect("fixture request must be UTF-8");
|
||||
}
|
||||
|
||||
fn request_complete(bytes: &[u8]) -> bool {
|
||||
let text = match std::str::from_utf8(bytes) {
|
||||
std::result::Result::Ok(text) => text,
|
||||
std::result::Result::Err(_) => return false,
|
||||
};
|
||||
let header_end = match text.find("\r\n\r\n") {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return false,
|
||||
};
|
||||
let mut content_length = 0_usize;
|
||||
for line in text[..header_end].lines() {
|
||||
let (name, value) = match line.split_once(':') {
|
||||
std::option::Option::Some(parts) => parts,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if name.eq_ignore_ascii_case("content-length") {
|
||||
content_length = value.trim().parse::<usize>().expect("content length must parse");
|
||||
}
|
||||
}
|
||||
return bytes.len() >= header_end.saturating_add(4).saturating_add(content_length);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn executor_applies_retry_after_and_retries_http_429_for_retry_safe_method() {
|
||||
let (url, handle) = serve_rate_limit_then_success();
|
||||
let pool = pool_for_url(url.as_str(), std::time::Duration::from_millis(500), 1);
|
||||
let result = pool
|
||||
.execute_standard_rpc(&crate::HttpRoleName::new("default"), health_method(), std::vec::Vec::new())
|
||||
.await
|
||||
.expect("retry-safe request must recover from one 429");
|
||||
assert_eq!(result, serde_json::json!("ok"));
|
||||
assert_eq!(handle.join().expect("fixture server must join"), 2);
|
||||
let snapshot = pool.snapshot();
|
||||
assert_eq!(snapshot.endpoints()[0].roles()[0].rate_limit_count(), 1);
|
||||
assert_eq!(snapshot.endpoints()[0].roles()[0].success_count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn executor_maps_reqwest_timeout_to_ksp_timeout_error_without_endpoint_secret_leak() {
|
||||
const SECRET_CANARY: &str = "SECRET-REQWEST-URL-CANARY";
|
||||
let (url, handle) = serve_timeout();
|
||||
let sensitive_url = format!("{url}/rpc?api-key={SECRET_CANARY}");
|
||||
let pool = pool_for_url(sensitive_url.as_str(), std::time::Duration::from_millis(20), 0);
|
||||
let error = pool
|
||||
.execute_standard_rpc(&crate::HttpRoleName::new("default"), health_method(), std::vec::Vec::new())
|
||||
.await
|
||||
.expect_err("timed out request must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
|
||||
assert!(!format!("{error:?}").contains(SECRET_CANARY));
|
||||
let source = std::error::Error::source(&error).expect("transport timeout should preserve a sanitized reqwest source");
|
||||
assert!(!format!("{source:?}").contains(SECRET_CANARY));
|
||||
handle.join().expect("fixture server must join");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName {
|
||||
return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate");
|
||||
@@ -55,7 +55,7 @@ fn yellowstone_subscribe_empty_and_named_empty_maps_encode_exactly() {
|
||||
assert_eq!(wire.slots.get("slots"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default()));
|
||||
assert_eq!(
|
||||
wire.transactions.get("transactions"),
|
||||
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default())
|
||||
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()),
|
||||
);
|
||||
assert_eq!(
|
||||
wire.transactions_status.get("transaction-status"),
|
||||
@@ -365,3 +365,191 @@ fn yellowstone_slot_update_preserves_all_current_statuses_and_bounds_dead_error(
|
||||
};
|
||||
assert!(super::decode_slot_update(oversized).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yellowstone_transaction_filters_encode_complete_current_wire_and_redact_selectors() {
|
||||
let mut filter = crate::YellowstoneSubscribeTransactionFilter::new();
|
||||
filter.set_vote(std::option::Option::Some(false));
|
||||
filter.set_failed(std::option::Option::Some(true));
|
||||
let signature = crate::YellowstoneTransactionSignatureSelector::new("1".repeat(64)).expect("signature selector must validate");
|
||||
filter.set_signature(std::option::Option::Some(signature));
|
||||
assert!(filter.push_account_include(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])).is_ok());
|
||||
assert!(filter.push_account_exclude(ksp_core_lib::Pubkey::new_from_array([2_u8; 32])).is_ok());
|
||||
assert!(filter.push_account_required(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
|
||||
let cuckoo =
|
||||
crate::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 7, crate::YellowstoneCuckooHashAlgorithm::SipHash).expect("cuckoo filter must validate");
|
||||
filter.set_cuckoo_account_include(std::option::Option::Some(cuckoo));
|
||||
filter.set_token_accounts(std::option::Option::Some(crate::YellowstoneTokenAccountExpansion::BalanceChanged));
|
||||
let wire = filter.to_wire();
|
||||
assert_eq!(wire.vote, std::option::Option::Some(false));
|
||||
assert_eq!(wire.failed, std::option::Option::Some(true));
|
||||
assert!(wire.signature.is_some());
|
||||
assert_eq!(wire.account_include.len(), 1);
|
||||
assert_eq!(wire.account_exclude.len(), 1);
|
||||
assert_eq!(wire.account_required.len(), 1);
|
||||
assert!(wire.cuckoo_account_include.is_some());
|
||||
assert_eq!(wire.token_accounts, std::option::Option::Some(yellowstone_grpc_proto::geyser::TokenAccountExpansionControlFlag::BalanceChanged as i32));
|
||||
let debug = format!("{filter:?}");
|
||||
assert!(debug.contains("account_include_count"));
|
||||
assert!(!debug.contains(&"1".repeat(32)));
|
||||
assert!(!debug.contains(&ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string()));
|
||||
assert!(crate::YellowstoneTransactionSignatureSelector::new("contains-0-O-I-l").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yellowstone_transaction_update_decodes_current_storage_wire_including_v1_config_and_meta() {
|
||||
let confirmed = yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
|
||||
signature: vec![9_u8; 64],
|
||||
is_vote: false,
|
||||
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
|
||||
signatures: vec![vec![9_u8; 64], vec![8_u8; 64]],
|
||||
message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message {
|
||||
header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader {
|
||||
num_required_signatures: 2,
|
||||
num_readonly_signed_accounts: 1,
|
||||
num_readonly_unsigned_accounts: 1,
|
||||
}),
|
||||
account_keys: vec![vec![1_u8; 32], vec![2_u8; 32]],
|
||||
recent_blockhash: vec![3_u8; 32],
|
||||
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::CompiledInstruction {
|
||||
program_id_index: 1,
|
||||
accounts: vec![0_u8, 1],
|
||||
data: vec![4_u8, 5, 6],
|
||||
}],
|
||||
versioned: true,
|
||||
address_table_lookups: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::MessageAddressTableLookup {
|
||||
account_key: vec![4_u8; 32],
|
||||
writable_indexes: vec![1_u8, 2],
|
||||
readonly_indexes: vec![3_u8],
|
||||
}],
|
||||
config: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionConfig {
|
||||
priority_fee: std::option::Option::Some(7),
|
||||
compute_unit_limit: std::option::Option::Some(8),
|
||||
loaded_accounts_data_size_limit: std::option::Option::Some(9),
|
||||
heap_size: std::option::Option::Some(10),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
meta: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta {
|
||||
err: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError { err: vec![11_u8, 12] }),
|
||||
fee: 5_000,
|
||||
pre_balances: vec![100, 200],
|
||||
post_balances: vec![90, 210],
|
||||
inner_instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions {
|
||||
index: 0,
|
||||
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstruction {
|
||||
program_id_index: 1,
|
||||
accounts: vec![0_u8],
|
||||
data: vec![13_u8, 14],
|
||||
stack_height: std::option::Option::Some(2),
|
||||
}],
|
||||
}],
|
||||
inner_instructions_none: false,
|
||||
log_messages: vec!["Program log: fixture".to_owned()],
|
||||
log_messages_none: false,
|
||||
pre_token_balances: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::TokenBalance {
|
||||
account_index: 0,
|
||||
mint: "mint-fixture".to_owned(),
|
||||
ui_token_amount: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UiTokenAmount {
|
||||
ui_amount: 1.5,
|
||||
decimals: 6,
|
||||
amount: "1500000".to_owned(),
|
||||
ui_amount_string: "1.5".to_owned(),
|
||||
}),
|
||||
owner: "owner-fixture".to_owned(),
|
||||
program_id: "program-fixture".to_owned(),
|
||||
}],
|
||||
post_token_balances: vec![],
|
||||
rewards: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::Reward {
|
||||
pubkey: ksp_core_lib::Pubkey::new_from_array([5_u8; 32]).to_string(),
|
||||
lamports: 17,
|
||||
post_balance: 18,
|
||||
reward_type: yellowstone_grpc_proto::solana::storage::confirmed_block::RewardType::Staking as i32,
|
||||
commission: "5".to_owned(),
|
||||
commission_bps: "500".to_owned(),
|
||||
}],
|
||||
loaded_writable_addresses: vec![vec![6_u8; 32]],
|
||||
loaded_readonly_addresses: vec![vec![7_u8; 32]],
|
||||
return_data: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::ReturnData {
|
||||
program_id: vec![8_u8; 32],
|
||||
data: vec![15_u8, 16],
|
||||
}),
|
||||
return_data_none: false,
|
||||
compute_units_consumed: std::option::Option::Some(123),
|
||||
cost_units: std::option::Option::Some(456),
|
||||
}),
|
||||
index: 3,
|
||||
};
|
||||
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||
filters: vec!["transactions-main".to_owned()],
|
||||
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Transaction(
|
||||
yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction { transaction: std::option::Option::Some(confirmed), slot: 42 },
|
||||
)),
|
||||
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 100, nanos: 200 }),
|
||||
};
|
||||
let update = super::decode_transaction_update(wire).expect("transaction update fixture must decode");
|
||||
assert_eq!(update.slot(), 42);
|
||||
assert_eq!(update.filters()[0].as_str(), "transactions-main");
|
||||
assert_eq!(update.transaction().signature().as_bytes(), &[9_u8; 64]);
|
||||
assert_eq!(format!("{:?}", update.transaction().signature()), "YellowstoneTransactionSignature(<redacted>)");
|
||||
assert_eq!(update.transaction().index(), 3);
|
||||
assert_eq!(update.transaction().transaction().signatures().len(), 2);
|
||||
assert!(update.transaction().transaction().message().versioned());
|
||||
let config = update.transaction().transaction().message().config().expect("v1 config must be preserved");
|
||||
assert_eq!(config.priority_fee(), std::option::Option::Some(7));
|
||||
assert_eq!(config.heap_size(), std::option::Option::Some(10));
|
||||
assert_eq!(update.transaction().meta().fee(), 5_000);
|
||||
assert_eq!(update.transaction().meta().error().expect("error must be present").as_bytes(), &[11_u8, 12]);
|
||||
assert_eq!(update.transaction().meta().inner_instructions()[0].instructions()[0].stack_height(), std::option::Option::Some(2));
|
||||
assert_eq!(update.transaction().meta().pre_token_balances()[0].ui_token_amount().expect("token amount must be present").amount(), "1500000");
|
||||
let token_debug = format!("{:?}", update.transaction().meta().pre_token_balances()[0]);
|
||||
assert!(!token_debug.contains("mint-fixture"));
|
||||
assert!(!token_debug.contains("owner-fixture"));
|
||||
assert!(!token_debug.contains("program-fixture"));
|
||||
assert_eq!(update.transaction().meta().rewards()[0].reward_type(), crate::YellowstoneRewardType::Staking);
|
||||
assert_eq!(update.transaction().meta().loaded_writable_addresses()[0], ksp_core_lib::Pubkey::new_from_array([6_u8; 32]));
|
||||
assert_eq!(update.transaction().meta().return_data().expect("return data must be present").data(), &[15_u8, 16]);
|
||||
assert_eq!(update.transaction().meta().compute_units_consumed(), std::option::Option::Some(123));
|
||||
assert_eq!(update.transaction().meta().cost_units(), std::option::Option::Some(456));
|
||||
let debug = format!("{update:?}");
|
||||
assert!(!debug.contains("Program log: fixture"));
|
||||
assert!(!debug.contains("11, 12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yellowstone_transaction_status_update_preserves_error_and_rejects_malformed_signature() {
|
||||
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||
filters: vec!["status".to_owned()],
|
||||
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(
|
||||
yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionStatus {
|
||||
slot: 55,
|
||||
signature: vec![2_u8; 64],
|
||||
is_vote: true,
|
||||
index: 4,
|
||||
err: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError { err: vec![99_u8] }),
|
||||
},
|
||||
)),
|
||||
created_at: std::option::Option::None,
|
||||
};
|
||||
let update = super::decode_transaction_status_update(wire).expect("transaction-status update must decode");
|
||||
assert_eq!(update.slot(), 55);
|
||||
assert!(update.is_vote());
|
||||
assert_eq!(update.index(), 4);
|
||||
assert_eq!(update.signature().as_bytes(), &[2_u8; 64]);
|
||||
assert_eq!(update.error().expect("status error must be present").as_bytes(), &[99_u8]);
|
||||
assert!(!format!("{update:?}").contains("99"));
|
||||
let malformed = yellowstone_grpc_proto::geyser::SubscribeUpdate {
|
||||
filters: vec!["status".to_owned()],
|
||||
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(
|
||||
yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionStatus {
|
||||
slot: 1,
|
||||
signature: vec![0_u8; 63],
|
||||
is_vote: false,
|
||||
index: 0,
|
||||
err: std::option::Option::None,
|
||||
},
|
||||
)),
|
||||
created_at: std::option::Option::None,
|
||||
};
|
||||
assert!(super::decode_transaction_status_update(malformed).is_err());
|
||||
}
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
||||
// version: 4
|
||||
|
||||
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
||||
return crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new(name),
|
||||
true,
|
||||
request_kinds,
|
||||
priority,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
}
|
||||
|
||||
fn endpoint(name: &str, enabled: bool, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointSettings {
|
||||
return crate::HttpEndpointSettings::new(
|
||||
name,
|
||||
enabled,
|
||||
crate::HttpProviderName::new("provider"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
crate::HttpEndpointUrl::parse(format!("https://{name}.invalid/rpc?token=SECRET-CANARY")).expect("test URL must parse"),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(2),
|
||||
std::option::Option::Some(4),
|
||||
std::vec![role("default", priority, request_kinds)],
|
||||
);
|
||||
}
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
|
||||
}
|
||||
|
||||
fn limited_endpoint(
|
||||
name: &str,
|
||||
priority: u32,
|
||||
requests_per_second: std::option::Option<u32>,
|
||||
burst_capacity: std::option::Option<u32>,
|
||||
max_concurrent_requests: std::option::Option<u32>,
|
||||
cooldown: std::option::Option<std::time::Duration>,
|
||||
) -> crate::HttpEndpointSettings {
|
||||
let limits = crate::HttpRoleLimits::new(
|
||||
requests_per_second.map(|value| return non_zero(value)),
|
||||
burst_capacity.map(|value| return non_zero(value)),
|
||||
max_concurrent_requests.map(|value| return non_zero(value)),
|
||||
cooldown,
|
||||
);
|
||||
let role = crate::HttpEndpointRoleSettings::new(crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], priority, limits);
|
||||
return crate::HttpEndpointSettings::new(
|
||||
name,
|
||||
true,
|
||||
crate::HttpProviderName::new("provider"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
crate::HttpEndpointUrl::parse(format!("https://{name}.invalid/rpc?token=SECRET-CANARY")).expect("test URL must parse"),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(2),
|
||||
std::option::Option::Some(4),
|
||||
std::vec![role],
|
||||
);
|
||||
}
|
||||
|
||||
fn settings(endpoints: std::vec::Vec<crate::HttpEndpointSettings>) -> crate::HttpTransportSettings {
|
||||
return crate::HttpTransportSettings::new(
|
||||
endpoints,
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(10), std::time::Duration::from_millis(50)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_prefers_lowest_priority_tier() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let selection = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||
.expect("selection must succeed");
|
||||
assert_eq!(selection.endpoint_name(), "primary");
|
||||
assert_eq!(selection.priority(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_round_robins_fairly_inside_best_priority_tier() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let first = pool.select_for_request_kind(&role, &kind).expect("first selection must succeed");
|
||||
let second = pool.select_for_request_kind(&role, &kind).expect("second selection must succeed");
|
||||
let third = pool.select_for_request_kind(&role, &kind).expect("third selection must succeed");
|
||||
assert_eq!(first.endpoint_name(), "one");
|
||||
assert_eq!(second.endpoint_name(), "two");
|
||||
assert_eq!(third.endpoint_name(), "one");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let selection = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||
.expect("fallback must be selected");
|
||||
assert_eq!(selection.endpoint_name(), "fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_filters_role_and_capability_before_priority() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]),
|
||||
endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let selection = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||
.expect("matching capability must be selected");
|
||||
assert_eq!(selection.endpoint_name(), "matching");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_returns_structured_error_when_no_endpoint_matches() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
.expect("pool must build");
|
||||
let error = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction"))
|
||||
.expect_err("unsupported request kind must fail selection");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_ENDPOINT_SELECTION_FAILED);
|
||||
assert!(!format!("{error:?}").contains("SECRET-CANARY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_method_selection_uses_registry_request_kind() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
.expect("pool must build");
|
||||
let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist");
|
||||
let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route");
|
||||
assert_eq!(selection.request_kind().as_str(), "get_balance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let snapshot = pool.snapshot();
|
||||
let rendered = format!("{snapshot:?} {pool:?}");
|
||||
assert_eq!(snapshot.endpoint_count(), 2);
|
||||
assert_eq!(snapshot.available_endpoint_count(), 1);
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
assert!(!rendered.contains(".invalid/rpc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_role_is_excluded_before_priority_selection() {
|
||||
let base = endpoint("disabled-role", true, 1, std::vec![crate::HttpRequestKind::wildcard()]);
|
||||
let disabled_role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
false,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
1,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let enabled_non_matching_role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("maintenance"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
1,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let disabled_role_endpoint = crate::HttpEndpointSettings::new(
|
||||
base.name(),
|
||||
true,
|
||||
base.provider().clone(),
|
||||
base.cluster().clone(),
|
||||
base.url().clone(),
|
||||
base.connect_timeout(),
|
||||
base.request_timeout(),
|
||||
base.max_idle_connections_per_host(),
|
||||
std::vec![disabled_role, enabled_non_matching_role],
|
||||
);
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
disabled_role_endpoint,
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let selection = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))
|
||||
.expect("enabled fallback role must be selected");
|
||||
assert_eq!(selection.endpoint_name(), "fallback");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_standard_method_is_rejected_before_endpoint_routing() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
|
||||
.expect("pool must build");
|
||||
let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist");
|
||||
let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint("primary", 1, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let first = pool.acquire_for_request_kind(&role, &kind).await.expect("first request must acquire primary");
|
||||
assert_eq!(first.selection().endpoint_name(), "primary");
|
||||
let second = pool.acquire_for_request_kind(&role, &kind).await.expect("second request must fall back while primary is saturated");
|
||||
assert_eq!(second.selection().endpoint_name(), "fallback");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint("primary", 1, std::option::Option::Some(1), std::option::Option::Some(1), std::option::Option::None, std::option::Option::None),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let first = pool.acquire_for_request_kind(&role, &kind).await.expect("first request must consume primary token");
|
||||
assert_eq!(first.selection().endpoint_name(), "primary");
|
||||
drop(first);
|
||||
let second = pool.acquire_for_request_kind(&role, &kind).await.expect("fallback must be used while primary token bucket refills");
|
||||
assert_eq!(second.selection().endpoint_name(), "fallback");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(std::time::Duration::from_millis(50)),
|
||||
),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let primary = pool.acquire_for_request_kind(&role, &kind).await.expect("primary must be acquired");
|
||||
assert_eq!(primary.selection().endpoint_name(), "primary");
|
||||
let pause = primary.record_rate_limited(std::option::Option::None);
|
||||
assert_eq!(pause, std::time::Duration::from_millis(50));
|
||||
drop(primary);
|
||||
let fallback = pool.acquire_for_request_kind(&role, &kind).await.expect("fallback must be selected during primary cooldown");
|
||||
assert_eq!(fallback.selection().endpoint_name(), "fallback");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_across_await() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::None,
|
||||
)]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let first = pool.acquire_for_request_kind(&role, &kind).await.expect("first permit must be acquired");
|
||||
let release_task = tokio::spawn(async move {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
drop(first);
|
||||
});
|
||||
let second = pool
|
||||
.acquire_for_request_kind_with_timeout(&role, &kind, std::time::Duration::from_millis(100))
|
||||
.await
|
||||
.expect("second permit must wake after concurrency release");
|
||||
assert_eq!(second.selection().endpoint_name(), "primary");
|
||||
release_task.await.expect("release task must complete");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(1),
|
||||
std::option::Option::None,
|
||||
)]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let _held = pool.acquire_for_request_kind(&role, &kind).await.expect("first permit must be acquired");
|
||||
let error = pool
|
||||
.acquire_for_request_kind_with_timeout(&role, &kind, std::time::Duration::from_millis(20))
|
||||
.await
|
||||
.expect_err("second permit must time out while concurrency remains saturated");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn passive_health_snapshot_moves_from_degraded_back_to_available_after_success() {
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
)]))
|
||||
.expect("pool must build");
|
||||
let role = crate::HttpRoleName::new("default");
|
||||
let kind = crate::HttpRequestKind::new("get_balance");
|
||||
let first = pool.acquire_for_request_kind(&role, &kind).await.expect("request permit must be acquired");
|
||||
first.record_failure();
|
||||
drop(first);
|
||||
let degraded = pool.snapshot();
|
||||
assert_eq!(degraded.endpoints()[0].availability(), crate::HttpEndpointAvailability::Degraded);
|
||||
assert_eq!(degraded.endpoints()[0].roles()[0].failure_count(), 1);
|
||||
let second = pool.acquire_for_request_kind(&role, &kind).await.expect("degraded endpoint remains eligible for passive recovery");
|
||||
second.record_success();
|
||||
drop(second);
|
||||
let recovered = pool.snapshot();
|
||||
assert_eq!(recovered.endpoints()[0].availability(), crate::HttpEndpointAvailability::Available);
|
||||
assert_eq!(recovered.endpoints()[0].roles()[0].success_count(), 1);
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
|
||||
}
|
||||
|
||||
fn retry_settings() -> crate::HttpRetrySettings {
|
||||
return crate::HttpRetrySettings::new(4, std::time::Duration::from_millis(100), std::time::Duration::from_millis(500));
|
||||
}
|
||||
|
||||
fn method(name: &str) -> &'static crate::HttpRpcMethodDescriptor {
|
||||
return crate::find_http_rpc_method(name).expect("audited test method must exist");
|
||||
}
|
||||
|
||||
fn role_limits(
|
||||
requests_per_second: std::option::Option<u32>,
|
||||
burst_capacity: std::option::Option<u32>,
|
||||
max_concurrent_requests: std::option::Option<u32>,
|
||||
cooldown: std::option::Option<std::time::Duration>,
|
||||
) -> crate::HttpEndpointRoleSettings {
|
||||
return crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
10,
|
||||
crate::HttpRoleLimits::new(
|
||||
requests_per_second.map(|value| return non_zero(value)),
|
||||
burst_capacity.map(|value| return non_zero(value)),
|
||||
max_concurrent_requests.map(|value| return non_zero(value)),
|
||||
cooldown,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_backoff_is_exponential_and_bounded() {
|
||||
let settings = retry_settings();
|
||||
assert_eq!(super::retry_backoff(&settings, 1), std::time::Duration::from_millis(100));
|
||||
assert_eq!(super::retry_backoff(&settings, 2), std::time::Duration::from_millis(200));
|
||||
assert_eq!(super::retry_backoff(&settings, 3), std::time::Duration::from_millis(400));
|
||||
assert_eq!(super::retry_backoff(&settings, 4), std::time::Duration::from_millis(500));
|
||||
assert_eq!(super::retry_backoff(&settings, 32), std::time::Duration::from_millis(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_safe_timeout_is_retried_until_budget_is_exhausted() {
|
||||
let settings = retry_settings();
|
||||
let first = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
crate::HttpRetryCause::Timeout,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(first, crate::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100)));
|
||||
let exhausted = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
crate::HttpRetryCause::Timeout,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
settings.max_retries(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(exhausted, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_never_retries_after_ambiguous_dispatch() {
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("sendTransaction"),
|
||||
&retry_settings(),
|
||||
crate::HttpRetryCause::Connection,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(decision, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_can_retry_when_transport_proves_no_dispatch() {
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("sendTransaction"),
|
||||
&retry_settings(),
|
||||
crate::HttpRetryCause::Connection,
|
||||
crate::HttpDispatchState::NotDispatched,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert!(decision.should_retry());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_application_and_invalid_response_are_not_transport_retries() {
|
||||
for cause in [crate::HttpRetryCause::RpcApplication, crate::HttpRetryCause::InvalidResponse, crate::HttpRetryCause::Request] {
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&retry_settings(),
|
||||
cause,
|
||||
crate::HttpDispatchState::NotDispatched,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(decision, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_retry_after_can_extend_backoff_but_is_defensively_bounded() {
|
||||
let settings = retry_settings();
|
||||
let extended = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
crate::HttpRetryCause::RateLimited,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::Some(std::time::Duration::from_secs(3)),
|
||||
);
|
||||
assert_eq!(extended.delay(), std::option::Option::Some(std::time::Duration::from_secs(3)));
|
||||
let bounded = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
crate::HttpRetryCause::RateLimited,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::Some(std::time::Duration::from_secs(600)),
|
||||
);
|
||||
assert_eq!(bounded.delay(), std::option::Option::Some(std::time::Duration::from_secs(60)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_bucket_consumes_burst_then_refills_from_elapsed_time() {
|
||||
let start = std::time::Instant::now();
|
||||
let mut bucket = super::HttpTokenBucketState::new(2, 2, start);
|
||||
assert!(bucket.try_consume_at(start).is_none());
|
||||
assert!(bucket.try_consume_at(start).is_none());
|
||||
assert!(bucket.try_consume_at(start).is_some());
|
||||
let later = start.checked_add(std::time::Duration::from_millis(500)).expect("test instant must advance");
|
||||
assert!(bucket.try_consume_at(later).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_burst_capacity_defaults_to_one_second_of_rps_capacity() {
|
||||
let role = role_limits(std::option::Option::Some(2), std::option::Option::None, std::option::Option::None, std::option::Option::None);
|
||||
let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let now = std::time::Instant::now();
|
||||
let first = runtime.try_acquire(now);
|
||||
let second = runtime.try_acquire(now);
|
||||
let third = runtime.try_acquire(now);
|
||||
assert!(matches!(first, crate::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(second, crate::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(third, crate::RoleAdmissionAttempt::BlockedUntil(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrency_semaphore_releases_capacity_when_permit_is_dropped() {
|
||||
let role = role_limits(std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None);
|
||||
let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let now = std::time::Instant::now();
|
||||
let first = runtime.try_acquire(now);
|
||||
let held = match first {
|
||||
crate::RoleAdmissionAttempt::Ready(permit) => permit,
|
||||
_ => panic!("first concurrency permit must be available"),
|
||||
};
|
||||
assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::ConcurrencySaturated));
|
||||
drop(held);
|
||||
assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::Ready(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_limit_cooldown_marks_role_and_caps_provider_delay() {
|
||||
let role = role_limits(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(std::time::Duration::from_millis(10)),
|
||||
);
|
||||
let runtime = crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
let pause = runtime.record_rate_limited(std::option::Option::Some(std::time::Duration::from_secs(600)));
|
||||
assert_eq!(pause, std::time::Duration::from_secs(60));
|
||||
assert_eq!(runtime.rate_limit_count(), 1);
|
||||
assert_eq!(runtime.failure_count(), 1);
|
||||
assert_eq!(runtime.availability(std::time::Instant::now()), crate::HttpEndpointAvailability::RateLimited);
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
|
||||
}
|
||||
|
||||
fn valid_settings(url_text: &str) -> crate::HttpTransportSettings {
|
||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
|
||||
let limits = crate::HttpRoleLimits::new(
|
||||
std::option::Option::Some(non_zero(10)),
|
||||
std::option::Option::Some(non_zero(20)),
|
||||
std::option::Option::Some(non_zero(4)),
|
||||
std::option::Option::Some(std::time::Duration::from_millis(500)),
|
||||
);
|
||||
let role = crate::HttpEndpointRoleSettings::new(crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], 100, limits);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"devnet_public",
|
||||
true,
|
||||
crate::HttpProviderName::new("solana-public"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(15),
|
||||
std::option::Option::Some(8),
|
||||
std::vec![role],
|
||||
);
|
||||
return crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_accepts_http_and_https() {
|
||||
assert!(crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
|
||||
assert!(crate::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_rejects_non_http_schemes() {
|
||||
let result = crate::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
|
||||
let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_debug_redacts_secret_material() {
|
||||
let url = crate::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
|
||||
let rendered = format!("{url:?}");
|
||||
assert!(rendered.contains("<redacted>"));
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
assert!(!rendered.contains("provider.invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_transport_settings_pass_validation() {
|
||||
let settings = valid_settings("https://api.devnet.solana.com");
|
||||
assert!(settings.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_debug_does_not_leak_endpoint_url() {
|
||||
let settings = valid_settings("https://provider.invalid/rpc?api-key=SECRET-CANARY");
|
||||
let rendered = format!("{settings:?}");
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
assert!(!rendered.contains("provider.invalid"));
|
||||
assert!(rendered.contains("HttpEndpointUrl(<redacted>)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_require_one_enabled_endpoint() {
|
||||
let url = crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
100,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"disabled",
|
||||
false,
|
||||
crate::HttpProviderName::new("provider"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::None,
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
);
|
||||
let error = settings.validate().expect_err("all-disabled settings must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_duplicate_endpoint_names() {
|
||||
let first = valid_settings("https://one.invalid");
|
||||
let second = valid_settings("https://two.invalid");
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
|
||||
let error = settings.validate().expect_err("duplicate endpoint names must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_duplicate_roles() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let duplicated_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
endpoint.cluster().clone(),
|
||||
endpoint.url().clone(),
|
||||
endpoint.connect_timeout(),
|
||||
endpoint.request_timeout(),
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard(), crate::HttpRequestKind::new("get_balance")],
|
||||
100,
|
||||
endpoint.roles()[0].limits().clone(),
|
||||
);
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
endpoint.cluster().clone(),
|
||||
endpoint.url().clone(),
|
||||
endpoint.connect_timeout(),
|
||||
endpoint.request_timeout(),
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_burst_without_rps() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
100,
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
endpoint.cluster().clone(),
|
||||
endpoint.url().clone(),
|
||||
endpoint.connect_timeout(),
|
||||
endpoint.request_timeout(),
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_reversed_retry_backoff() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
base.endpoints().to_vec(),
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||
);
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_zero_request_timeout() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
endpoint.cluster().clone(),
|
||||
endpoint.url().clone(),
|
||||
endpoint.connect_timeout(),
|
||||
std::time::Duration::ZERO,
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
endpoint.roles().to_vec(),
|
||||
);
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
165
deltas/0.2.9/pre.007.md
Normal file
165
deltas/0.2.9/pre.007.md
Normal file
@@ -0,0 +1,165 @@
|
||||
<!-- file: deltas/0.2.9/pre.007.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.9-pre.007` — Yellowstone Transactions + `transaction_status`
|
||||
|
||||
## 1. Base et gate précédent
|
||||
|
||||
Base exacte :
|
||||
|
||||
```text
|
||||
0.2.9-pre.006
|
||||
```
|
||||
|
||||
Le gate opérateur `pre.006` est fermé : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 release-completeness + 4 doctests, dependency canary Core 3/3 PASS.
|
||||
|
||||
Version technique de cette tranche :
|
||||
|
||||
```text
|
||||
0.2.9-pre.7
|
||||
```
|
||||
|
||||
Commit attendu après gate vert :
|
||||
|
||||
```text
|
||||
v0.2.9-pre.007
|
||||
```
|
||||
|
||||
Aucun tag Git stable n'est créé pour cette prerelease.
|
||||
|
||||
## 2. Objet de la tranche
|
||||
|
||||
`pre.007` complète exclusivement les deux familles Yellowstone standard `transactions` et `transactions_status` du `SubscribeRequest`/`SubscribeUpdate` courant. Elle n'ouvre toujours pas le stream bidi runtime.
|
||||
|
||||
Le proto publié `yellowstone-grpc-proto 12.6.0` a été réaudité avant implémentation. Le même `SubscribeRequestFilterTransactions` est utilisé par les deux maps et expose :
|
||||
|
||||
```text
|
||||
vote?
|
||||
failed?
|
||||
signature?
|
||||
account_include[]
|
||||
account_exclude[]
|
||||
account_required[]
|
||||
cuckoo_account_include?
|
||||
token_accounts? = ALL | BALANCE_CHANGED
|
||||
```
|
||||
|
||||
## 3. Contrat request KSP
|
||||
|
||||
La tranche matérialise `YellowstoneSubscribeTransactionFilter` et les types associés :
|
||||
|
||||
```text
|
||||
YellowstoneTransactionSignatureSelector
|
||||
YellowstoneTokenAccountExpansion
|
||||
YellowstoneCuckooFilter réutilisé
|
||||
```
|
||||
|
||||
La signature textuelle est :
|
||||
|
||||
- non vide ;
|
||||
- bornée à 128 octets ;
|
||||
- strictement Base58 ;
|
||||
- décodée par KSP pour vérifier une largeur exacte de 64 octets ;
|
||||
- absente des diagnostics et du `Debug`.
|
||||
|
||||
Les listes include/exclude/required conservent l'ordre d'insertion et réutilisent le bound déterministe des sélecteurs Accounts. Le Cuckoo transaction réutilise le contrat standard KSP déjà introduit en `pre.005`.
|
||||
|
||||
## 4. Updates et `solana-storage.proto`
|
||||
|
||||
Les nouveaux DTOs KSP couvrent les variantes :
|
||||
|
||||
```text
|
||||
SubscribeUpdateTransaction
|
||||
SubscribeUpdateTransactionStatus
|
||||
```
|
||||
|
||||
et projettent sans raw reexport upstream les structures utiles :
|
||||
|
||||
```text
|
||||
Transaction / Message / MessageHeader
|
||||
CompiledInstruction / MessageAddressTableLookup
|
||||
TransactionConfig V1
|
||||
TransactionStatusMeta / TransactionError
|
||||
InnerInstructions / InnerInstruction
|
||||
TokenBalance / UiTokenAmount
|
||||
ReturnData / Reward
|
||||
```
|
||||
|
||||
Le `Message.config` optionnel actuel est conservé avec :
|
||||
|
||||
```text
|
||||
priority_fee?
|
||||
compute_unit_limit?
|
||||
loaded_accounts_data_size_limit?
|
||||
heap_size?
|
||||
```
|
||||
|
||||
`TransactionStatusMeta` conserve séparément les marqueurs legacy `inner_instructions_none`, `log_messages_none` et `return_data_none`, ainsi que les payloads correspondants. Les champs optionnels `compute_units_consumed` et `cost_units`, les loaded addresses, rewards et token balances sont également préservés.
|
||||
|
||||
`TransactionError.err` reste un payload opaque borné ; KSP n'introduit aucun décodage JSON ou Program arbitraire.
|
||||
|
||||
## 5. Validation et sûreté
|
||||
|
||||
Les décodeurs test-only vérifient notamment :
|
||||
|
||||
| Élément | Politique `pre.007` |
|
||||
|------------------------------------------------------|----------------------------------------------------------------------------|
|
||||
| signatures wire | exactement 64 octets |
|
||||
| hash / account keys / loaded addresses / program ids | exactement 32 octets |
|
||||
| vecteurs transaction/meta | bornes KSP avant projection |
|
||||
| instruction / return-data payloads | bornés |
|
||||
| logs et textes provider | count/length bornés |
|
||||
| error bytes | opaques et bornés |
|
||||
| `Debug` | pas de signature, pubkey, log, instruction data ou error bytes arbitraires |
|
||||
|
||||
Les conversions protobuf request et les décodeurs update restent `#[cfg(test)]` jusqu'à `pre.009`, premier consommateur runtime prévu lors de l'ouverture du stream bidi.
|
||||
|
||||
## 6. Frontières préservées
|
||||
|
||||
Toujours hors `pre.007` :
|
||||
|
||||
```text
|
||||
Blocks + block_meta + entry pre.008
|
||||
bidi/backpressure/Ping-Pong/shutdown pre.009
|
||||
reconnect/replay/gaps/duplicates pre.010
|
||||
Config V3 + PublicNode profiles pre.011
|
||||
PublicNode live/compliance pre.012
|
||||
SubscribeDeshred OUT 0.2.9 standard
|
||||
```
|
||||
|
||||
Aucun provider N3 n'est ajouté. Aucune dépendance ni feature Cargo n'est modifiée.
|
||||
|
||||
## 7. Preuves ajoutées
|
||||
|
||||
Tests Transport ajoutés :
|
||||
|
||||
```text
|
||||
transaction filter exact wire + redaction + Base58 width
|
||||
transaction update storage fixture + TransactionConfig V1 + meta
|
||||
transaction_status update + error + malformed signature
|
||||
public API root contract
|
||||
release-completeness exact scope
|
||||
```
|
||||
|
||||
Compte cible après compilation :
|
||||
|
||||
```text
|
||||
Transport unit ~367
|
||||
Transport public API 46
|
||||
Transport release completeness 40
|
||||
Transport doctests 4
|
||||
```
|
||||
|
||||
## 8. Gate opérateur requis
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test -p ksp-core-lib --test workspace_dependencies
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Aucun `cargo tree` supplémentaire n'est requis : aucune dépendance ou feature ne change dans cette tranche.
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
|
||||
|
||||
> **Statut : `0.2.9-pre.005-fix.001` est fermée sur gate opérateur intégralement vert et sans warning : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 38 completeness + 4 doctests. `0.2.9-pre.006` est une tranche structurelle dédiée au nommage des modules privés : les cinq modules sans ambiguïté HTTP (`client`, `executor`, `pool`, `resilience`, `settings`) deviennent `http_*`, avec leurs unit tests miroirs. Les modules `rpc_*`, `json_rpc`, `constants` et `error` ne sont pas préfixés artificiellement car ils décrivent une couche/protocole partagé ou portent déjà des DTOs utilisés par WS/gRPC. Cette insertion décale l’ancien forecast fonctionnel `pre.006–011` vers `pre.007–012` sans changer son contenu. Aucun contrat public ni comportement runtime ne change.**
|
||||
> **Statut : `0.2.9-pre.006` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 completeness + 4 doctests. `0.2.9-pre.007` est candidate et matérialise uniquement Transactions + `transaction_status` du `Subscribe` standard : filtres complets, signature Base58 décodée exactement sur 64 octets, Cuckoo/token-account expansion, DTOs transaction/meta complets et Transaction V1 `TransactionConfig`. Blocks restent `pre.008`; aucun stream bidi n’est encore ouvert.**
|
||||
|
||||
## 1. Objet, base et état d'ouverture
|
||||
|
||||
@@ -925,11 +925,11 @@ pre.004 DONE — standard Solana : Subscribe foundation + maps/commitment/ping/
|
||||
pre.005 DONE — standard Solana : Accounts + Slots filters/updates
|
||||
budget : 15–20 min ; gate final fix.001 : fmt/audit/check/Clippy/workspace PASS sans warning + Transport 364/45/38/4
|
||||
|
||||
pre.006 CANDIDATE — structure Transport : namespace privé HTTP explicite
|
||||
budget : 15–20 min ; preuve : 5 modules + 5 unit tests renommés `http_*`, API publique inchangée, canari d'absence des anciens modules
|
||||
pre.006 DONE — structure Transport : namespace privé HTTP explicite
|
||||
budget : 15–20 min ; gate opérateur final PASS 364/45/39/4 + workspace
|
||||
|
||||
pre.007 standard Solana : Transactions + transaction_status
|
||||
budget : 15–20 min ; preuve : include/exclude/required/Cuckoo/token expansion + tx/meta
|
||||
pre.007 CANDIDATE — standard Solana : Transactions + transaction_status
|
||||
budget : 15–20 min ; preuve : include/exclude/required/Cuckoo/token expansion + tx/meta + TransactionConfig V1
|
||||
|
||||
pre.008 standard Solana : Blocks + block_meta + entry
|
||||
budget : 15–20 min ; preuve : counts/arrays/optional/oneof/payload bounds
|
||||
@@ -1216,3 +1216,60 @@ Cette tranche est volontairement séparée de Transactions afin de respecter le
|
||||
|
||||
**Gate candidat :** audit Rust clean, public API inchangée, cinq anciens modules absents après suppression opérateur, cinq nouveaux modules `http_*` compilés, release-completeness canary dédié, workspace complet vert.
|
||||
|
||||
|
||||
## 21. Gate final `pre.006` — namespace privé HTTP
|
||||
|
||||
Preuve opérateur du `2026-08-24` :
|
||||
|
||||
| Gate | Résultat final |
|
||||
|------------------------------------------|----------------|
|
||||
| `cargo fmt --all` | PASS |
|
||||
| audit Rust workspace | PASS / clean |
|
||||
| `cargo check --workspace` | PASS |
|
||||
| `cargo clippy --workspace --all-targets` | PASS |
|
||||
| Transport unit | 364/364 PASS |
|
||||
| Transport `public_api` | 45/45 PASS |
|
||||
| Transport `release_completeness` | 39/39 PASS |
|
||||
| Transport doctests | 4/4 PASS |
|
||||
| Core dependency canary | 3/3 PASS |
|
||||
| `cargo test --workspace` | PASS |
|
||||
|
||||
Les cinq modules privés HTTP et leurs unit tests miroirs sont désormais effectivement nommés `http_*`. Le canari `release_v0_2_9_pre_006_namespaces_unambiguously_http_owned_private_modules` passe et les modules partagés `rpc_*`/`json_rpc` restent volontairement non préfixés.
|
||||
|
||||
**Verdict : `pre.006` fermée.**
|
||||
|
||||
## 22. `pre.007` — Transactions + `transaction_status` candidate
|
||||
|
||||
Le proto `yellowstone-grpc-proto 12.6.0` est réaudité avant implémentation. `SubscribeRequestFilterTransactions` contient exactement `vote?`, `failed?`, `signature?`, `account_include[]`, `account_exclude[]`, `account_required[]`, `cuckoo_account_include?` et `token_accounts?`. La même structure filtre `transactions` et `transactions_status`.
|
||||
|
||||
La candidate matérialise :
|
||||
|
||||
```text
|
||||
request filters
|
||||
vote? / failed?
|
||||
signature? : Base58 validé et décodé exactement sur 64 octets
|
||||
account_include[] / account_exclude[] / account_required[]
|
||||
cuckoo_account_include?
|
||||
token_accounts? = ALL | BALANCE_CHANGED
|
||||
|
||||
updates
|
||||
transaction : filters + created_at + slot + signature/is_vote/index + transaction + meta
|
||||
transaction_status : filters + created_at + slot + signature/is_vote/index + error?
|
||||
|
||||
solana-storage typed
|
||||
Transaction / Message / MessageHeader
|
||||
CompiledInstruction / MessageAddressTableLookup
|
||||
TransactionConfig V1 (priority_fee/compute_unit_limit/loaded_accounts_data_size_limit/heap_size)
|
||||
TransactionStatusMeta + TransactionError
|
||||
InnerInstructions / InnerInstruction
|
||||
TokenBalance / UiTokenAmount
|
||||
ReturnData / Reward
|
||||
loaded writable/readonly addresses
|
||||
compute_units_consumed? / cost_units?
|
||||
```
|
||||
|
||||
Les marqueurs `inner_instructions_none`, `log_messages_none` et `return_data_none` sont conservés séparément de leurs payloads. Les erreurs transaction sont des bytes opaques bornés : aucun décodage Program/runtime arbitraire n'est introduit. Les types protobuf upstream restent internes. Les conversions request et les décodeurs update restent `#[cfg(test)]` jusqu'à l'ouverture du stream runtime en `pre.009`.
|
||||
|
||||
OUT de `pre.007` : Blocks/block_meta/entry, stream bidi/Ping-Pong, reconnect/replay, Config V3, PublicNode et `SubscribeDeshred`.
|
||||
|
||||
**Gate candidat :** audit statique clean ; compilation/Clippy/tests opérateur requis avant fermeture.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Validation `0.2.9` — moteur Yellowstone + standard Solana + PublicNode
|
||||
|
||||
> **Statut : `0.2.9-pre.005-fix.001` est fermé sur gate opérateur intégralement vert et sans warning : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 38 completeness + 4 doctests. `pre.006` est une tranche structurelle de namespace privé : `client/executor/pool/resilience/settings` deviennent `http_*` avec leurs tests miroirs, sans changement d’API publique. Les `rpc_*`, `json_rpc`, `constants` et `error` restent volontairement non préfixés lorsqu’ils sont partagés ou déjà groupés. Le forecast fonctionnel est décalé d’un numéro : Transactions `pre.007`, Blocks `pre.008`, bidi `pre.009`, reconnect `pre.010`, Config V3 `pre.011`, PublicNode final `pre.012`.**
|
||||
> **Statut : `pre.006` est fermée sur gate opérateur intégralement vert : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 completeness + 4 doctests. `0.2.9-pre.007` est candidate Transactions + `transaction_status` : filtre current complet, signature Base58 -> 64 octets, Cuckoo/token expansion, updates transaction/status et projection typée complète du `solana-storage.proto` courant incluant Transaction V1 `TransactionConfig`. Blocks restent `pre.008`; bidi reste `pre.009`.**
|
||||
|
||||
## 1. Autorités du gate
|
||||
|
||||
@@ -167,6 +167,7 @@ Transaction
|
||||
Message
|
||||
MessageHeader
|
||||
MessageAddressTableLookup
|
||||
TransactionConfig
|
||||
TransactionStatusMeta
|
||||
TransactionError
|
||||
InnerInstructions / InnerInstruction
|
||||
@@ -407,8 +408,8 @@ pre.002 DONE moteur: deps/settings/errors/channel 15
|
||||
pre.003 DONE TLS/metadata + fixture + 7 unary standard 15–20 min ; gate final fix.001 PASS
|
||||
pre.004 DONE standard: Subscribe common/from_slot/bounds 15–20 min ; gate final fix.001 PASS
|
||||
pre.005 DONE standard: accounts + slots 15–20 min ; gate final fix.001 PASS
|
||||
pre.006 CANDIDATE structure: namespace privé HTTP `http_*` 15–20 min
|
||||
pre.007 TODO standard: transactions + transaction_status 15–20 min
|
||||
pre.006 DONE structure: namespace privé HTTP `http_*` 15–20 min ; gate PASS
|
||||
pre.007 CANDIDATE standard: transactions + transaction_status 15–20 min
|
||||
pre.008 TODO standard: blocks + block_meta + entry 15–20 min
|
||||
pre.009 TODO moteur: bidi/backpressure/half-close/shutdown 15–20 min
|
||||
pre.010 TODO moteur: reconnect/replay/gap/duplicate 15–20 min
|
||||
@@ -760,3 +761,55 @@ Le renommage est limité aux cinq modules dont l'ownership HTTP est sans ambigu
|
||||
|
||||
**Verdict `pre.006` : candidate structurelle prête ; fermeture après suppressions opérateur et gate Cargo complet.**
|
||||
|
||||
|
||||
## 22. Gate final `pre.006`
|
||||
|
||||
| Gate | Résultat final |
|
||||
|------------------------------------------|----------------|
|
||||
| `cargo fmt --all` | PASS |
|
||||
| audit Rust workspace | PASS / clean |
|
||||
| `cargo check --workspace` | PASS |
|
||||
| `cargo clippy --workspace --all-targets` | PASS |
|
||||
| Transport unit | 364/364 PASS |
|
||||
| Transport `public_api` | 45/45 PASS |
|
||||
| Transport `release_completeness` | 39/39 PASS |
|
||||
| Transport doctests | 4/4 PASS |
|
||||
| Core dependency canary | 3/3 PASS |
|
||||
| `cargo test --workspace` | PASS |
|
||||
|
||||
**Verdict : `pre.006` fermée.**
|
||||
|
||||
## 23. Gate `pre.007` — Transactions + `transaction_status` candidate
|
||||
|
||||
| Surface | État candidate |
|
||||
|--------------------------------------------------------------------|----------------|
|
||||
| workspace version | `0.2.9-pre.7` |
|
||||
| `vote?` / `failed?` | SOURCE+TEST |
|
||||
| signature Base58 décodée exactement 64 octets | SOURCE+TEST |
|
||||
| include/exclude/required ordonnés et bornés | SOURCE+TEST |
|
||||
| Cuckoo transaction include | SOURCE+TEST |
|
||||
| token expansion ALL/BALANCE_CHANGED | SOURCE+TEST |
|
||||
| `SubscribeUpdateTransaction` | SOURCE+TEST |
|
||||
| `SubscribeUpdateTransactionStatus` | SOURCE+TEST |
|
||||
| Transaction/Message/Header | SOURCE+TEST |
|
||||
| compiled instructions / ALT lookups | SOURCE+TEST |
|
||||
| Transaction V1 `TransactionConfig` | SOURCE+TEST |
|
||||
| TransactionStatusMeta complet | SOURCE+TEST |
|
||||
| inner instructions + legacy none marker | SOURCE+TEST |
|
||||
| logs + legacy none marker | SOURCE+TEST |
|
||||
| token balances / UiTokenAmount | SOURCE+TEST |
|
||||
| loaded addresses | SOURCE+TEST |
|
||||
| ReturnData + legacy none marker | SOURCE+TEST |
|
||||
| rewards | SOURCE+TEST |
|
||||
| compute_units_consumed / cost_units | SOURCE+TEST |
|
||||
| opaque TransactionError borné | SOURCE+TEST |
|
||||
| Debug sans signatures/token-balance text/payloads/logs/error bytes | SOURCE+TEST |
|
||||
| Blocks / block_meta / entry | OUT pre.007 |
|
||||
| stream bidi / lifecycle | OUT pre.007 |
|
||||
| PublicNode / Config V3 | OUT pre.007 |
|
||||
| audit Rust workspace local | PASS / clean |
|
||||
| fmt/check/Clippy/tests | opérateur TODO |
|
||||
|
||||
Le proto 12.6.0 ajoute à la représentation de transaction le `Message.config` optionnel pour Transaction V1 / SIMD-0385 ; la candidate le conserve explicitement. Les marqueurs legacy `inner_instructions_none`, `log_messages_none` et `return_data_none` ne sont pas fusionnés avec leurs collections/messages.
|
||||
|
||||
**Verdict `pre.007` : candidate source prête ; fermeture après gate Cargo opérateur.**
|
||||
|
||||
Reference in New Issue
Block a user