/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
//! JSON-RPC 2.0 object definitions
use std::collections::HashMap;
use rand::{rngs::OsRng, Rng};
use tinyjson::JsonValue;
use crate::{
error::RpcError,
system::{Publisher, PublisherPtr},
Result,
};
/// Parse a JSON field into i64. Accepts numeric values and numeric strings.
/// Note this is not fully spec-compliant, but the vast majority of RPC
/// clients use numeric IDs.
fn parse_id_field(v: &JsonValue, accept_string: bool) -> std::result::Result {
let n = if let Some(num) = v.get::() {
*num
} else if accept_string {
match v.get::() {
Some(s) => s
.parse::()
.map_err(|_| RpcError::InvalidJson("id string is not numeric".to_string()))?,
None => return Err(RpcError::InvalidJson("id is not a number or string".to_string())),
}
} else {
return Err(RpcError::InvalidJson("id is not a number".to_string()))
};
if !n.is_finite() || n.fract() != 0.0 {
return Err(RpcError::InvalidJson("id must be a finite integer".to_string()))
}
if n < i64::MIN as f64 || n > i64::MAX as f64 {
return Err(RpcError::InvalidJson("id out of i64 range".to_string()))
}
Ok(n as i64)
}
/// Parse a JSON number into i32 with the same bounds-checking discipline
fn parse_i32_field(v: &JsonValue, name: &str) -> std::result::Result {
let n =
*v.get::().ok_or_else(|| RpcError::InvalidJson(format!("{name} is not a number")))?;
if !n.is_finite() || n.fract() != 0.0 {
return Err(RpcError::InvalidJson(format!("{name} must be a finite integer")))
}
if n < i32::MIN as f64 || n > i32::MAX as f64 {
return Err(RpcError::InvalidJson(format!("{name} out of i32 range")))
}
Ok(n as i32)
}
/// JSON-RPC error codes.
/// The error codes `[-32768, -32000]` are reserved for predefined errors.
#[derive(Copy, Clone, Debug)]
pub enum ErrorCode {
/// Invalid JSON was received by the server.
/// An error occurred on the server while parsing the JSON text.
ParseError,
/// The JSON sent is not a valid Request object.
InvalidRequest,
/// The method does not exist / is not available.
MethodNotFound,
/// Invalid method parameter(s).
InvalidParams,
/// Internal JSON-RPC error.
InternalError,
/// ID mismatch
IdMismatch,
/// Invalid/Unexpected reply
InvalidReply,
/// Reserved for implementation-defined server-errors.
ServerError(i32),
}
impl ErrorCode {
pub fn code(&self) -> i32 {
match *self {
Self::ParseError => -32700,
Self::InvalidRequest => -32600,
Self::MethodNotFound => -32601,
Self::InvalidParams => -32602,
Self::InternalError => -32603,
Self::IdMismatch => -32360,
Self::InvalidReply => -32361,
Self::ServerError(c) => c,
}
}
pub fn message(&self) -> String {
match *self {
Self::ParseError => "parse error".to_string(),
Self::InvalidRequest => "invalid request".to_string(),
Self::MethodNotFound => "method not found".to_string(),
Self::InvalidParams => "invalid params".to_string(),
Self::InternalError => "internal error".to_string(),
Self::IdMismatch => "id mismatch".to_string(),
Self::InvalidReply => "invalid reply".to_string(),
Self::ServerError(_) => "server error".to_string(),
}
}
pub fn desc(&self) -> JsonValue {
JsonValue::String(self.message())
}
}
// ANCHOR: jsonresult
/// Wrapping enum around the available JSON-RPC object types
#[derive(Clone, Debug)]
pub enum JsonResult {
Response(JsonResponse),
Error(JsonError),
Notification(JsonNotification),
/// Subscriber is a special object that yields a channel
Subscriber(JsonSubscriber),
SubscriberWithReply(JsonSubscriber, JsonResponse),
Request(JsonRequest),
}
impl JsonResult {
pub fn try_from_value(value: &JsonValue) -> Result {
if let Ok(response) = JsonResponse::try_from(value) {
return Ok(Self::Response(response))
}
if let Ok(error) = JsonError::try_from(value) {
return Ok(Self::Error(error))
}
if let Ok(notification) = JsonNotification::try_from(value) {
return Ok(Self::Notification(notification))
}
Err(RpcError::InvalidJson("Invalid JSON Result".to_string()).into())
}
}
impl From for JsonResult {
fn from(resp: JsonResponse) -> Self {
Self::Response(resp)
}
}
impl From for JsonResult {
fn from(err: JsonError) -> Self {
Self::Error(err)
}
}
impl From for JsonResult {
fn from(notif: JsonNotification) -> Self {
Self::Notification(notif)
}
}
impl From for JsonResult {
fn from(sub: JsonSubscriber) -> Self {
Self::Subscriber(sub)
}
}
impl From<(JsonSubscriber, JsonResponse)> for JsonResult {
fn from(tuple: (JsonSubscriber, JsonResponse)) -> Self {
Self::SubscriberWithReply(tuple.0, tuple.1)
}
}
// ANCHOR: jsonrequest
/// A JSON-RPC request object
#[derive(Clone, Debug)]
pub struct JsonRequest {
/// JSON-RPC version
pub jsonrpc: &'static str,
/// Request ID
pub id: i64,
/// Request method
pub method: String,
/// Request parameters
pub params: JsonValue,
}
// ANCHOR_END: jsonrequest
impl JsonRequest {
/// Create a new [`JsonRequest`] object with the given method and parameters.
/// The request ID is chosen randomly.
pub fn new(method: &str, params: JsonValue) -> Self {
assert!(params.is_object() || params.is_array());
let id: i64 = OsRng::gen_range(&mut OsRng, 0..(1i64 << 53));
Self { jsonrpc: "2.0", id, method: method.to_string(), params }
}
/// Convert the object into a JSON string
pub fn stringify(&self) -> Result {
let v: JsonValue = self.into();
Ok(v.stringify()?)
}
}
impl From<&JsonRequest> for JsonValue {
fn from(req: &JsonRequest) -> JsonValue {
JsonValue::Object(HashMap::from([
("jsonrpc".to_string(), JsonValue::String(req.jsonrpc.to_string())),
("id".to_string(), JsonValue::Number(req.id as f64)),
("method".to_string(), JsonValue::String(req.method.clone())),
("params".to_string(), req.params.clone()),
]))
}
}
impl TryFrom<&JsonValue> for JsonRequest {
type Error = RpcError;
fn try_from(value: &JsonValue) -> std::result::Result {
if !value.is_object() {
return Err(RpcError::InvalidJson("JSON is not an Object".to_string()))
}
// We have to allocate the value here another time in order to mutate
// it if necessary.
let mut value = value.clone();
let map: &mut HashMap = value.get_mut().unwrap();
if !map.contains_key("jsonrpc") ||
!map["jsonrpc"].is_string() ||
map["jsonrpc"] != JsonValue::String("2.0".to_string())
{
return Err(RpcError::InvalidJson(
"Request does not contain valid \"jsonrpc\" field".to_string(),
))
}
if !map.contains_key("id")
/* || !map["id"].is_number() */
{
return Err(RpcError::InvalidJson(
"Request does not contain valid \"id\" field".to_string(),
))
}
if !map.contains_key("method") || !map["method"].is_string() {
return Err(RpcError::InvalidJson(
"Request does not contain valid \"method\" field".to_string(),
))
}
if !map.contains_key("params") {
// HACK ALERT:
// On nonexisting `params`, we'll just make them into something.
map.insert("params".to_string(), JsonValue::from(vec![]));
}
if !map["params"].is_object() && !map["params"].is_array() {
return Err(RpcError::InvalidJson(
"Request does not contain valid \"params\" field".to_string(),
))
}
let id = parse_id_field(&map["id"], true)?;
Ok(Self {
jsonrpc: "2.0",
id,
method: map["method"].get::().unwrap().clone(),
params: map["params"].clone(),
})
}
}
/// A JSON-RPC notification object
#[derive(Clone, Debug)]
pub struct JsonNotification {
/// JSON-RPC version
pub jsonrpc: &'static str,
/// Notification method
pub method: String,
/// Notification parameters
pub params: JsonValue,
}
impl JsonNotification {
/// Create a new [`JsonNotification`] object with the given method and parameters.
pub fn new(method: &str, params: JsonValue) -> Self {
assert!(params.is_object() || params.is_array());
Self { jsonrpc: "2.0", method: method.to_string(), params }
}
/// Convert the object into a JSON string
pub fn stringify(&self) -> Result {
let v: JsonValue = self.into();
Ok(v.stringify()?)
}
}
impl From<&JsonNotification> for JsonValue {
fn from(notif: &JsonNotification) -> JsonValue {
JsonValue::Object(HashMap::from([
("jsonrpc".to_string(), JsonValue::String(notif.jsonrpc.to_string())),
("method".to_string(), JsonValue::String(notif.method.clone())),
("params".to_string(), notif.params.clone()),
]))
}
}
impl TryFrom<&JsonValue> for JsonNotification {
type Error = RpcError;
fn try_from(value: &JsonValue) -> std::result::Result {
if !value.is_object() {
return Err(RpcError::InvalidJson("JSON is not an Object".to_string()))
}
let map: &HashMap = value.get().unwrap();
if !map.contains_key("jsonrpc") ||
!map["jsonrpc"].is_string() ||
map["jsonrpc"] != JsonValue::String("2.0".to_string())
{
return Err(RpcError::InvalidJson(
"Notification does not contain valid \"jsonrpc\" field".to_string(),
))
}
if !map.contains_key("method") || !map["method"].is_string() {
return Err(RpcError::InvalidJson(
"Notification does not contain valid \"method\" field".to_string(),
))
}
if !map.contains_key("params") {
return Err(RpcError::InvalidJson(
"Notification does not contain valid \"params\" field".to_string(),
))
}
if !map["params"].is_object() && !map["params"].is_array() {
return Err(RpcError::InvalidJson(
"Request does not contain valid \"params\" field".to_string(),
))
}
Ok(Self {
jsonrpc: "2.0",
method: map["method"].get::().unwrap().clone(),
params: map["params"].clone(),
})
}
}
/// A JSON-RPC response object
#[derive(Clone, Debug)]
pub struct JsonResponse {
/// JSON-RPC version
pub jsonrpc: &'static str,
/// Request ID
pub id: i64,
/// Response result
pub result: JsonValue,
}
impl JsonResponse {
/// Create a new [`JsonResponse`] object with the given ID and result value.
/// Creating a `JsonResponse` implies that the method call was successful.
pub fn new(result: JsonValue, id: i64) -> Self {
Self { jsonrpc: "2.0", id, result }
}
/// Convert the object into a JSON string
pub fn stringify(&self) -> Result {
let v: JsonValue = self.into();
Ok(v.stringify()?)
}
}
impl From<&JsonResponse> for JsonValue {
fn from(rep: &JsonResponse) -> JsonValue {
JsonValue::Object(HashMap::from([
("jsonrpc".to_string(), JsonValue::String(rep.jsonrpc.to_string())),
("id".to_string(), JsonValue::Number(rep.id as f64)),
("result".to_string(), rep.result.clone()),
]))
}
}
impl TryFrom<&JsonValue> for JsonResponse {
type Error = RpcError;
fn try_from(value: &JsonValue) -> std::result::Result {
if !value.is_object() {
return Err(RpcError::InvalidJson("Json is not an Object".to_string()))
}
let map: &HashMap = value.get().unwrap();
if !map.contains_key("jsonrpc") ||
!map["jsonrpc"].is_string() ||
map["jsonrpc"] != JsonValue::String("2.0".to_string())
{
return Err(RpcError::InvalidJson(
"Response does not contain valid \"jsonrpc\" field".to_string(),
))
}
if !map.contains_key("id") || !map["id"].is_number() {
return Err(RpcError::InvalidJson(
"Response does not contain valid \"id\" field".to_string(),
))
}
if !map.contains_key("result") {
return Err(RpcError::InvalidJson(
"Response does not contain valid \"result\" field".to_string(),
))
}
Ok(Self {
jsonrpc: "2.0",
id: parse_id_field(&map["id"], false)?,
result: map["result"].clone(),
})
}
}
impl TryFrom for JsonResponse {
type Error = RpcError;
/// Converts [`JsonResult`] to [`JsonResponse`], returning the response or an `InvalidJson`
/// error if the structure is not a `JsonResponse`.
fn try_from(result: JsonResult) -> std::result::Result {
match result {
JsonResult::Response(response) => Ok(response),
_ => Err(RpcError::InvalidJson("Not a JsonResult::Response".to_string())),
}
}
}
/// A JSON-RPC error object
#[derive(Clone, Debug)]
pub struct JsonError {
/// JSON-RPC version
pub jsonrpc: &'static str,
/// Request ID
pub id: i64,
/// JSON-RPC error (code and message)
pub error: JsonErrorVal,
}
/// A JSON-RPC error value (code and message)
#[derive(Clone, Debug)]
pub struct JsonErrorVal {
/// Error code
pub code: i32,
/// Error message
pub message: String,
}
impl JsonError {
/// Create a new [`JsonError`] object with the given error code, optional
/// message, and a response ID.
/// Creating a `JsonError` implies that the method call was unsuccessful.
pub fn new(c: ErrorCode, message: Option, id: i64) -> Self {
let error = JsonErrorVal { code: c.code(), message: message.unwrap_or(c.message()) };
Self { jsonrpc: "2.0", id, error }
}
/// Convert the object into a JSON string
pub fn stringify(&self) -> Result {
let v: JsonValue = self.into();
Ok(v.stringify()?)
}
}
impl From<&JsonError> for JsonValue {
fn from(err: &JsonError) -> JsonValue {
let errmap = JsonValue::Object(HashMap::from([
("code".to_string(), JsonValue::Number(err.error.code.into())),
("message".to_string(), JsonValue::String(err.error.message.clone())),
]));
JsonValue::Object(HashMap::from([
("jsonrpc".to_string(), JsonValue::String(err.jsonrpc.to_string())),
("id".to_string(), JsonValue::Number(err.id as f64)),
("error".to_string(), errmap),
]))
}
}
impl TryFrom for JsonError {
type Error = RpcError;
/// Converts [`JsonResult`] to [`JsonError`], returning the response or an `InvalidJson`
/// error if the structure is not a `JsonError`.
fn try_from(result: JsonResult) -> std::result::Result {
match result {
JsonResult::Error(error) => Ok(error),
_ => Err(RpcError::InvalidJson("Not a JsonResult::Error".to_string())),
}
}
}
impl TryFrom<&JsonValue> for JsonError {
type Error = RpcError;
fn try_from(value: &JsonValue) -> std::result::Result {
if !value.is_object() {
return Err(RpcError::InvalidJson("JSON is not an Object".to_string()))
}
let map: &HashMap = value.get().unwrap();
if !map.contains_key("jsonrpc") ||
!map["jsonrpc"].is_string() ||
map["jsonrpc"] != JsonValue::String("2.0".to_string())
{
return Err(RpcError::InvalidJson(
"Error does not contain valid \"jsonrpc\" field".to_string(),
))
}
if !map.contains_key("id") || !map["id"].is_number() {
return Err(RpcError::InvalidJson(
"Error does not contain valid \"id\" field".to_string(),
))
}
if !map.contains_key("error") || !map["error"].is_object() {
return Err(RpcError::InvalidJson(
"Error does not contain valid \"error\" field".to_string(),
))
}
let err_map: &HashMap = map["error"].get().unwrap();
let code_val = err_map.get("code").ok_or_else(|| {
RpcError::InvalidJson("Error does not contain \"error.code\" field".to_string())
})?;
let message_val = err_map.get("message").ok_or_else(|| {
RpcError::InvalidJson("Error does not contain \"error.message\" field".to_string())
})?;
let code = parse_i32_field(code_val, "error.code")?;
let message = message_val
.get::()
.ok_or_else(|| RpcError::InvalidJson("\"error.message\" is not a string".to_string()))?
.to_string();
Ok(Self {
jsonrpc: "2.0",
id: parse_id_field(&map["id"], false)?,
error: JsonErrorVal { code, message },
})
}
}
/// A JSON-RPC subscriber for notifications
#[derive(Clone, Debug)]
pub struct JsonSubscriber {
/// Notification method
pub method: &'static str,
/// Notification publisher
pub publisher: PublisherPtr,
}
impl JsonSubscriber {
pub fn new(method: &'static str) -> Self {
let publisher = Publisher::new();
Self { method, publisher }
}
/// Send a notification to the publisher with the given JSON object
pub async fn notify(&self, params: JsonValue) {
let notification = JsonNotification::new(self.method, params);
self.publisher.notify(notification).await;
}
}
/// Parses a [`JsonValue`] parameter into a `String`.
/// Returns the string if successful or an error if the value is not a valid string.
pub fn parse_json_string(name: &str, value: &JsonValue) -> std::result::Result {
value
.get::()
.cloned()
.ok_or_else(|| RpcError::InvalidJson(format!("Parameter '{name}' is not a valid string")))
}
/// Parses a [`JsonValue`] parameter into a `f64`.
/// Returns the number if successful or an error if the value is not a valid number.
pub fn parse_json_number(name: &str, value: &JsonValue) -> std::result::Result {
value.get::().cloned().ok_or_else(|| {
RpcError::InvalidJson(format!("Parameter '{name}' is not a supported number type"))
})
}
/// Parses the element at the specified index in a [`JsonValue::Array`] into a
/// string. Returns the string if successful, or an error if the parameter is
/// missing, not an array, or not a valid string.
pub fn parse_json_array_string(
name: &str,
index: usize,
array_value: &JsonValue,
) -> std::result::Result {
match array_value {
JsonValue::Array(values) => values
.get(index)
.ok_or_else(|| {
RpcError::InvalidJson(format!("Parameter '{name}' at index {index} is missing"))
})
.and_then(|param| parse_json_string(name, param)),
_ => Err(RpcError::InvalidJson(format!("Parameter '{name}' is not an array"))),
}
}
/// Parses the element at the specified index in a [`JsonValue::Array`] into an
/// `f64` (compatible with [`JsonValue::Number`]). Returns the number if successful,
/// or an error if the parameter is missing, not an array, or is not a valid number.
pub fn parse_json_array_number(
name: &str,
index: usize,
array_value: &JsonValue,
) -> std::result::Result {
match array_value {
JsonValue::Array(values) => values
.get(index)
.ok_or_else(|| {
RpcError::InvalidJson(format!("Parameter '{name}' at index {index} is missing"))
})
.and_then(|param| parse_json_number(name, param)),
_ => Err(RpcError::InvalidJson(format!("Parameter '{name}' is not an array"))),
}
}
/// Attempts to parse a `JsonResult`, converting it into a `JsonResponse` and
/// extracting a string result from it. Returns an error if conversion or
/// extraction fails, and the extracted string on success.
pub fn parse_json_response_string(
json_result: JsonResult,
) -> std::result::Result {
// Try converting `JsonResult` into a `JsonResponse`.
let json_response: JsonResponse = json_result.try_into().map_err(|_| {
RpcError::InvalidJson("Failed to convert JsonResult into JsonResponse".to_string())
})?;
// Attempt to extract a string result from the JsonResponse
json_response.result.get::().map(|value| value.to_string()).ok_or_else(|| {
RpcError::InvalidJson("Failed to parse string from JsonResponse result".to_string())
})
}
/// Converts the provided JSON-RPC parameters into an array of JSON values,
/// returning a reference to the array if successful, or a JsonResult error containing a
/// JsonError when the input is not a JSON array.
pub fn to_json_array(params: &JsonValue) -> std::result::Result<&Vec, RpcError> {
if let JsonValue::Array(array) = params {
Ok(array)
} else {
Err(RpcError::InvalidJson(
"Expected an array of values, but received a different JSON type.".to_string(),
))
}
}
/// Validates whether the provided JSON parameter is an empty array or object, returning success if it is empty or an Error if it contains values.
pub fn validate_empty_params(params: &JsonValue) -> std::result::Result<(), RpcError> {
match to_json_array(params) {
Ok(array) if array.is_empty() => Ok(()),
Ok(_) => Err(RpcError::InvalidJson(format!(
"Parameters not permited, received: {:?}",
params.stringify().unwrap_or("Error converting JSON to string".to_string())
))),
Err(err) => Err(RpcError::InvalidJson(err.to_string())),
}
}