Files
khadhroony-solana-project/crates/ksp-onchain-transport-lib/unit_tests/ws_blocks.rs
2026-08-23 08:58:03 +02:00

223 lines
12 KiB
Rust

// file: crates/ksp-onchain-transport-lib/unit_tests/ws_blocks.rs
// version: 1
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
fn fixture_pubkey() -> ksp_core_lib::Pubkey {
return "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
}
fn local_endpoint(url: &str) -> crate::WsEndpointSettings {
return crate::WsEndpointSettings::new(
"local_ws_blocks",
true,
crate::WsProviderName::new("local-fixture"),
crate::WsClusterName::new("local"),
crate::WsProtocolKind::SolanaStandard,
crate::WsEndpointUrl::parse(url).expect("local test WebSocket URL must parse"),
crate::WsSessionSettings::default(),
);
}
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
let address = listener.local_addr().expect("local listener must expose address");
return (listener, format!("ws://{address}"));
}
async fn read_request(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> serde_json::Value {
let message = websocket.next().await.expect("request message must exist").expect("request message must decode");
let text = message.to_text().expect("request must be text");
return serde_json::from_str(text).expect("request must contain JSON");
}
async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, request: &serde_json::Value, result: serde_json::Value) {
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"result":result});
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local response must send");
}
async fn send_error(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, request: &serde_json::Value, code: i64, message: &str) {
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"error":{"code":code,"message":message}});
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local error response must send");
}
async fn send_notification(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, remote_id: u64, result: serde_json::Value) {
let notification = serde_json::json!({"jsonrpc":"2.0","method":"blockNotification","params":{"result":result,"subscription":remote_id}});
websocket.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await.expect("local notification must send");
}
async fn wait_for_close_frame(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) {
loop {
let message = websocket.next().await;
match message {
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
std::option::Option::Some(std::result::Result::Ok(_)) => {},
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
}
}
}
#[test]
fn block_subscribe_config_preserves_all_unstable_options_and_rejects_processed_commitment() {
let config = crate::SolanaBlockSubscribeConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed),
std::option::Option::Some(crate::SolanaTransactionDetails::Accounts),
std::option::Option::Some(7),
std::option::Option::Some(true),
);
assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed));
assert_eq!(config.encoding(), std::option::Option::Some(crate::SolanaTransactionEncoding::JsonParsed));
assert_eq!(config.transaction_details(), std::option::Option::Some(crate::SolanaTransactionDetails::Accounts));
assert_eq!(config.max_supported_transaction_version(), std::option::Option::Some(7));
assert_eq!(config.show_rewards(), std::option::Option::Some(true));
assert_eq!(
config.to_json_value(),
serde_json::json!({
"commitment": "confirmed",
"encoding": "jsonParsed",
"transactionDetails": "accounts",
"maxSupportedTransactionVersion": 7,
"showRewards": true
})
);
let invalid = crate::SolanaBlockSubscribeConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Processed),
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
std::option::Option::None,
);
assert_eq!(invalid.validate().expect_err("processed commitment must be rejected").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
}
#[test]
fn block_notification_decoder_preserves_nulls_and_shared_confirmed_block_shape() {
let nulls = super::decode_block_notification(
"blockSubscribe",
serde_json::json!({"context":{"slot":51},"value":{"slot":51,"block":null,"err":{"reason":"missing"}}}),
)
.expect("nullable block notification must decode");
assert_eq!(nulls.context().slot(), 51);
assert_eq!(nulls.value().slot(), 51);
assert!(nulls.value().block().is_none());
assert_eq!(nulls.value().err(), std::option::Option::Some(&serde_json::json!({"reason":"missing"})));
let block = super::decode_block_notification(
"blockSubscribe",
serde_json::json!({
"context":{"slot":52},
"value":{
"slot":52,
"block":{
"previousBlockhash":"prev",
"blockhash":"current",
"parentSlot":51,
"signatures":["sig-a"],
"rewards":null,
"numRewardPartitions":4,
"blockTime":123,
"blockHeight":9
},
"err":null
}
}),
)
.expect("shared confirmed block shape must decode");
assert_eq!(block.value().block().expect("block must exist").blockhash(), "current");
assert!(block.value().err().is_none());
let large_transaction = "A".repeat(2_048);
let large_wire = serde_json::json!({
"context":{"slot":53},
"value":{
"slot":53,
"block":{
"previousBlockhash":"prev",
"blockhash":"large-current",
"parentSlot":52,
"transactions":[{"transaction":[large_transaction,"base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}],
"rewards":[],
"blockTime":123,
"blockHeight":10
},
"err":null
}
});
assert!(large_wire.to_string().len() > 1_232);
assert!(super::decode_block_notification("blockSubscribe", large_wire).is_ok());
}
#[tokio::test(flavor = "current_thread")]
async fn unstable_block_wrapper_uses_exact_request_notification_and_handle_unsubscribe() {
let (listener, url) = bind_local_listener().await;
let pubkey = fixture_pubkey();
let server_pubkey = pubkey;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let subscribe = read_request(&mut websocket).await;
assert_eq!(subscribe["method"], serde_json::json!("blockSubscribe"));
assert_eq!(
subscribe["params"],
serde_json::json!([
{"mentionsAccountOrProgram":server_pubkey.to_string()},
{"commitment":"confirmed","encoding":"base64","transactionDetails":"signatures","maxSupportedTransactionVersion":3,"showRewards":false}
])
);
send_result(&mut websocket, &subscribe, serde_json::json!(301)).await;
send_notification(&mut websocket, 301, serde_json::json!({"context":{"slot":77},"value":{"slot":77,"block":null,"err":null}})).await;
let unsubscribe = read_request(&mut websocket).await;
assert_eq!(unsubscribe["method"], serde_json::json!("blockUnsubscribe"));
assert_eq!(unsubscribe["params"], serde_json::json!([301]));
send_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await;
wait_for_close_frame(&mut websocket).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let config = crate::SolanaBlockSubscribeConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
std::option::Option::Some(crate::SolanaTransactionEncoding::Base64),
std::option::Option::Some(crate::SolanaTransactionDetails::Signatures),
std::option::Option::Some(3),
std::option::Option::Some(false),
);
let mut subscription = session
.block_subscribe(&crate::SolanaBlockSubscribeFilter::MentionsAccountOrProgram(pubkey), std::option::Option::Some(&config))
.await
.expect("blockSubscribe must register");
let notification = subscription.recv().await.expect("block notification must arrive").expect("block notification must decode");
assert_eq!(notification.value().slot(), 77);
assert!(notification.value().block().is_none());
assert!(subscription.unsubscribe().await.expect("block unsubscribe must complete"));
session.close().await.expect("session close must complete");
server.await.expect("local server task must complete");
}
#[tokio::test(flavor = "current_thread")]
async fn unstable_block_validator_capability_rpc_error_does_not_fail_physical_session() {
let (listener, url) = bind_local_listener().await;
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("local server must accept client");
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
let block_subscribe = read_request(&mut websocket).await;
assert_eq!(block_subscribe["method"], serde_json::json!("blockSubscribe"));
send_error(&mut websocket, &block_subscribe, -32601, "block subscription disabled").await;
let root_subscribe = read_request(&mut websocket).await;
assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe"));
send_result(&mut websocket, &root_subscribe, serde_json::json!(302)).await;
wait_for_close_frame(&mut websocket).await;
});
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
let error = session
.block_subscribe(&crate::SolanaBlockSubscribeFilter::All, std::option::Option::None)
.await
.expect_err("validator capability application error must surface to the caller");
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
assert_eq!(session.state(), crate::WsSessionState::Active);
let root = session.root_subscribe().await.expect("session must remain usable after block application error");
assert_eq!(root.state(), crate::WsSubscriptionState::Active);
session.close().await.expect("session close must complete");
server.await.expect("local server task must complete");
}