// file: crates/ksp-onchain-transport-lib/src/json_rpc.rs // version: 1 const JSON_RPC_VERSION: &str = "2.0"; /// JSON-RPC 2.0 HTTP request envelope emitted by KSP. #[derive(Clone, PartialEq, serde::Serialize)] pub struct JsonRpcRequest { jsonrpc: &'static str, id: u64, method: std::string::String, params: std::vec::Vec, } impl JsonRpcRequest { /// Creates a JSON-RPC 2.0 request with a KSP-owned numeric identifier. pub fn new(id: u64, method: impl std::convert::Into, params: std::vec::Vec) -> ksp_core_lib::Result { let method = method.into(); if method.trim().is_empty() || method.trim() != method { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, "JSON-RPC method must be a non-empty trimmed string") .with_context("field", "method"), ); } return std::result::Result::Ok(Self { jsonrpc: JSON_RPC_VERSION, id, method, params }); } /// Returns the numeric request identifier. #[must_use] pub const fn id(&self) -> u64 { return self.id; } /// Returns the RPC method name. #[must_use] pub fn method(&self) -> &str { return self.method.as_str(); } /// Returns ordered request parameters. #[must_use] pub fn params(&self) -> &[serde_json::Value] { return self.params.as_slice(); } /// Serializes the request into compact JSON text. pub fn to_json_string(&self) -> ksp_core_lib::Result { let serialization_result = serde_json::to_string(self); return match serialization_result { std::result::Result::Ok(text) => std::result::Result::Ok(text), std::result::Result::Err(error) => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_ENCODE_FAILED, "cannot encode JSON-RPC request") .with_context("method", self.method()) .with_source(error), ), }; } } impl std::fmt::Debug for JsonRpcRequest { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("JsonRpcRequest") .field("jsonrpc", &self.jsonrpc) .field("id", &self.id) .field("method", &self.method) .field("param_count", &self.params.len()) .finish(); } } /// JSON-RPC 2.0 error payload returned by a remote Solana endpoint. #[derive(Clone, PartialEq, serde::Deserialize, serde::Serialize)] pub struct JsonRpcErrorObject { code: i64, message: std::string::String, #[serde(default)] data: std::option::Option, } impl JsonRpcErrorObject { /// Returns the remote JSON-RPC application error code. #[must_use] pub const fn code(&self) -> i64 { return self.code; } /// Returns the remote human-readable RPC error message. #[must_use] pub fn message(&self) -> &str { return self.message.as_str(); } /// Returns optional provider-supplied error data. #[must_use] pub const fn data(&self) -> std::option::Option<&serde_json::Value> { return self.data.as_ref(); } } impl std::fmt::Debug for JsonRpcErrorObject { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("JsonRpcErrorObject") .field("code", &self.code) .field("message", &"") .field("data", &if self.data.is_some() { "present" } else { "absent" }) .finish(); } } /// Validated JSON-RPC 2.0 success response. #[derive(Clone, PartialEq)] pub struct JsonRpcSuccessResponse { id: u64, result: serde_json::Value, } impl JsonRpcSuccessResponse { /// Returns the echoed request identifier. #[must_use] pub const fn id(&self) -> u64 { return self.id; } /// Returns the raw JSON result, including JSON `null` when the method legitimately returns it. #[must_use] pub const fn result(&self) -> &serde_json::Value { return &self.result; } /// Consumes the response and returns its raw JSON result. #[must_use] pub fn into_result(self) -> serde_json::Value { return self.result; } } impl std::fmt::Debug for JsonRpcSuccessResponse { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.debug_struct("JsonRpcSuccessResponse").field("id", &self.id).field("result", &"").finish(); } } /// Validated JSON-RPC 2.0 error response. #[derive(Clone, Debug, PartialEq)] pub struct JsonRpcErrorResponse { id: u64, error: crate::JsonRpcErrorObject, } impl JsonRpcErrorResponse { /// Returns the echoed request identifier. #[must_use] pub const fn id(&self) -> u64 { return self.id; } /// Returns the remote JSON-RPC application error payload. #[must_use] pub const fn error(&self) -> &crate::JsonRpcErrorObject { return &self.error; } } /// Validated JSON-RPC 2.0 HTTP response preserving success and application-error payloads separately. #[derive(Clone, Debug, PartialEq)] pub enum JsonRpcResponse { /// Successful response containing a raw method result. Success(crate::JsonRpcSuccessResponse), /// Application-level JSON-RPC error returned by the remote endpoint. Error(crate::JsonRpcErrorResponse), } impl JsonRpcResponse { /// Converts a validated response into the raw success result or a KSP application-error classification. pub fn into_result(self) -> ksp_core_lib::Result { return match self { Self::Success(success) => std::result::Result::Ok(success.into_result()), Self::Error(error_response) => std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_RPC_APPLICATION_ERROR, "Solana JSON-RPC endpoint returned an application error") .with_context("rpc_code", error_response.error().code().to_string()), ), }; } } /// Parses and validates a JSON-RPC HTTP response from UTF-8 JSON text. pub fn parse_json_rpc_response_text(text: &str, expected_id: u64) -> ksp_core_lib::Result { let decode_result = serde_json::from_str::(text); let value = match decode_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_DECODE_FAILED, "cannot decode JSON-RPC response as JSON").with_source(error), ); }, }; return crate::parse_json_rpc_response_value(value, expected_id); } /// Validates a decoded JSON value as one JSON-RPC HTTP response for the expected KSP request identifier. pub fn parse_json_rpc_response_value(value: serde_json::Value, expected_id: u64) -> ksp_core_lib::Result { let object = match value.as_object() { std::option::Option::Some(object) => object, std::option::Option::None => { return protocol_error("JSON-RPC response must be an object", "response"); }, }; let version = match object.get("jsonrpc") { std::option::Option::Some(serde_json::Value::String(version)) => version.as_str(), std::option::Option::Some(_) => { return protocol_error("JSON-RPC version must be a string", "jsonrpc"); }, std::option::Option::None => { return protocol_error("JSON-RPC response is missing its version", "jsonrpc"); }, }; if version != JSON_RPC_VERSION { return protocol_error("JSON-RPC version must be exactly 2.0", "jsonrpc"); } let response_id = match object.get("id") { std::option::Option::Some(id) => match id.as_u64() { std::option::Option::Some(id) => id, std::option::Option::None => { return protocol_error("JSON-RPC response id must be an unsigned integer", "id"); }, }, std::option::Option::None => { return protocol_error("JSON-RPC response is missing its id", "id"); }, }; if response_id != expected_id { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, "JSON-RPC response id does not match the request") .with_context("expected_id", expected_id.to_string()) .with_context("actual_id", response_id.to_string()), ); } let has_result = object.contains_key("result"); let has_error = object.contains_key("error"); if has_result == has_error { return protocol_error("JSON-RPC response must contain exactly one of result or error", "response"); } if has_result { let result = match object.get("result") { std::option::Option::Some(result) => result.clone(), std::option::Option::None => { return protocol_error("JSON-RPC result field disappeared during validation", "result"); }, }; return std::result::Result::Ok(crate::JsonRpcResponse::Success(crate::JsonRpcSuccessResponse { id: response_id, result })); } let error_value = match object.get("error") { std::option::Option::Some(error) => error.clone(), std::option::Option::None => { return protocol_error("JSON-RPC error field disappeared during validation", "error"); }, }; let error_decode_result = serde_json::from_value::(error_value); let error = match error_decode_result { std::result::Result::Ok(error) => error, std::result::Result::Err(error) => { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, "JSON-RPC error object is invalid") .with_context("field", "error") .with_source(error), ); }, }; return std::result::Result::Ok(crate::JsonRpcResponse::Error(crate::JsonRpcErrorResponse { id: response_id, error })); } fn protocol_error(message: &str, field: &str) -> ksp_core_lib::Result { return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID, message).with_context("field", field)); } #[cfg(test)] #[path = "../unit_tests/json_rpc.rs"] mod tests;