281 lines
10 KiB
Rust
281 lines
10 KiB
Rust
// 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<serde_json::Value>,
|
|
}
|
|
|
|
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<std::string::String>, params: std::vec::Vec<serde_json::Value>) -> ksp_core_lib::Result<Self> {
|
|
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<std::string::String> {
|
|
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<serde_json::Value>,
|
|
}
|
|
|
|
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", &"<redacted>")
|
|
.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", &"<omitted>").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<serde_json::Value> {
|
|
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<crate::JsonRpcResponse> {
|
|
let decode_result = serde_json::from_str::<serde_json::Value>(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<crate::JsonRpcResponse> {
|
|
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::<crate::JsonRpcErrorObject>(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<crate::JsonRpcResponse> {
|
|
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;
|