// file: crates/ksp-onchain-transport-lib/unit_tests/ws_accounts.rs // version: 1 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import fn local_endpoint(url: &str) -> crate::WsEndpointSettings { return crate::WsEndpointSettings::new( "local_ws_accounts", 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) -> 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, 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_notification(websocket: &mut tokio_tungstenite::WebSocketStream, method: &str, remote_id: u64, result: serde_json::Value) { let notification = serde_json::json!({"jsonrpc":"2.0","method":method,"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) { 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, } } } fn account_wire(lamports: u64) -> serde_json::Value { return serde_json::json!({ "lamports": lamports, "data": ["AQID", "base64"], "owner": "11111111111111111111111111111111", "executable": false, "rentEpoch": 7, "space": 3 }); } #[test] fn account_subscribe_config_preserves_effective_websocket_options_without_min_context_slot() { let config = crate::SolanaAccountSubscribeConfig::new( std::option::Option::Some(crate::SolanaAccountEncoding::Base64Zstd), std::option::Option::Some(crate::SolanaDataSliceConfig::new(4, 16)), std::option::Option::Some(crate::SolanaCommitment::Confirmed), ); assert_eq!(config.encoding(), std::option::Option::Some(crate::SolanaAccountEncoding::Base64Zstd)); assert_eq!(config.data_slice(), std::option::Option::Some(crate::SolanaDataSliceConfig::new(4, 16))); assert_eq!(config.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed)); assert_eq!(config.to_json_value(), serde_json::json!({"encoding":"base64+zstd","dataSlice":{"offset":4,"length":16},"commitment":"confirmed"})); assert!(config.to_json_value().get("minContextSlot").is_none()); } #[test] fn program_subscribe_config_preserves_filters_with_context_and_deterministic_bounds() { let config = crate::SolanaProgramSubscribeConfig::new( crate::SolanaAccountSubscribeConfig::new( std::option::Option::Some(crate::SolanaAccountEncoding::Base64), std::option::Option::None, std::option::Option::Some(crate::SolanaCommitment::Finalized), ), std::vec![ crate::SolanaProgramAccountFilter::DataSize(80), crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(4, crate::SolanaMemcmpBytes::Bytes(std::vec![1, 2, 3]))), crate::SolanaProgramAccountFilter::TokenAccountState, ], std::option::Option::Some(true), ); assert_eq!(config.with_context(), std::option::Option::Some(true)); assert_eq!(config.filters().len(), 3); assert_eq!( config.to_json_value(), serde_json::json!({ "encoding":"base64", "commitment":"finalized", "filters":[{"dataSize":80},{"memcmp":{"offset":4,"bytes":[1,2,3],"encoding":"bytes"}},"tokenAccountState"], "withContext":true }) ); let too_many = std::vec![ crate::SolanaProgramAccountFilter::DataSize(1), crate::SolanaProgramAccountFilter::DataSize(2), crate::SolanaProgramAccountFilter::DataSize(3), crate::SolanaProgramAccountFilter::DataSize(4), crate::SolanaProgramAccountFilter::DataSize(5), ]; let error = super::validate_program_subscribe_filters(too_many.as_slice()).expect_err("five programSubscribe filters must reject locally"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); let oversized = std::vec![crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Bytes(std::vec![0; 129]),))]; let error = super::validate_program_subscribe_filters(oversized.as_slice()).expect_err("oversized raw memcmp bytes must reject locally"); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); } #[test] fn program_notification_decoder_accepts_contextual_and_non_contextual_wire_forms() { let keyed = serde_json::json!({"pubkey":"11111111111111111111111111111111","account":account_wire(42)}); let bare = super::decode_program_notification("programSubscribe", keyed.clone()).expect("bare program notification must decode"); assert!(bare.context().is_none()); assert_eq!(bare.account().account().lamports(), 42); let contextual = super::decode_program_notification("programSubscribe", serde_json::json!({"context":{"slot":99,"apiVersion":"4.2.1"},"value":keyed})) .expect("contextual program notification must decode"); assert_eq!(contextual.context().expect("context must be retained").slot(), 99); assert_eq!(contextual.account().account().lamports(), 42); } #[tokio::test(flavor = "current_thread")] async fn stable_account_and_program_wrappers_use_exact_methods_decode_notifications_and_unsubscribe_by_handle() { 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 account_request = read_request(&mut websocket).await; assert_eq!(account_request["method"], serde_json::json!("accountSubscribe")); assert_eq!( account_request["params"], serde_json::json!(["11111111111111111111111111111111", {"encoding":"base64","dataSlice":{"offset":1,"length":2},"commitment":"confirmed"}]) ); send_result(&mut websocket, &account_request, serde_json::json!(51)).await; send_notification(&mut websocket, "accountNotification", 51, serde_json::json!({"context":{"slot":700},"value":account_wire(123)})).await; let account_unsubscribe = read_request(&mut websocket).await; assert_eq!(account_unsubscribe["method"], serde_json::json!("accountUnsubscribe")); assert_eq!(account_unsubscribe["params"], serde_json::json!([51])); send_result(&mut websocket, &account_unsubscribe, serde_json::json!(true)).await; let program_request = read_request(&mut websocket).await; assert_eq!(program_request["method"], serde_json::json!("programSubscribe")); assert_eq!( program_request["params"], serde_json::json!(["11111111111111111111111111111111", {"encoding":"jsonParsed","filters":[{"dataSize":80}],"withContext":true}]) ); send_result(&mut websocket, &program_request, serde_json::json!(73)).await; send_notification( &mut websocket, "programNotification", 73, serde_json::json!({ "context":{"slot":701}, "value":{"pubkey":"11111111111111111111111111111111","account":account_wire(456)} }), ) .await; let program_unsubscribe = read_request(&mut websocket).await; assert_eq!(program_unsubscribe["method"], serde_json::json!("programUnsubscribe")); assert_eq!(program_unsubscribe["params"], serde_json::json!([73])); send_result(&mut websocket, &program_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 account_pubkey = "11111111111111111111111111111111".parse::().expect("account pubkey fixture must parse"); let account_config = crate::SolanaAccountSubscribeConfig::new( std::option::Option::Some(crate::SolanaAccountEncoding::Base64), std::option::Option::Some(crate::SolanaDataSliceConfig::new(1, 2)), std::option::Option::Some(crate::SolanaCommitment::Confirmed), ); let mut account = session.account_subscribe(&account_pubkey, std::option::Option::Some(&account_config)).await.expect("accountSubscribe must register"); assert_eq!(account.kind(), crate::WsSubscriptionKind::Account); let notification = account.recv().await.expect("account notification must arrive").expect("account notification must decode"); assert_eq!(notification.context().slot(), 700); assert_eq!(notification.value().lamports(), 123); assert!(account.unsubscribe().await.expect("account unsubscribe must complete")); let program_config = crate::SolanaProgramSubscribeConfig::new( crate::SolanaAccountSubscribeConfig::new( std::option::Option::Some(crate::SolanaAccountEncoding::JsonParsed), std::option::Option::None, std::option::Option::None, ), std::vec![crate::SolanaProgramAccountFilter::DataSize(80)], std::option::Option::Some(true), ); let mut program = session.program_subscribe(&account_pubkey, std::option::Option::Some(&program_config)).await.expect("programSubscribe must register"); assert_eq!(program.kind(), crate::WsSubscriptionKind::Program); let notification = program.recv().await.expect("program notification must arrive").expect("program notification must decode"); assert_eq!(notification.context().expect("program context must be retained").slot(), 701); assert_eq!(notification.account().account().lamports(), 456); assert!(program.unsubscribe().await.expect("program unsubscribe must complete")); session.close().await.expect("session close must complete"); server.await.expect("local server task must complete"); }