darkfi/rpc/
jsonrpc.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19//! JSON-RPC 2.0 object definitions
20use std::collections::HashMap;
21
22use rand::{rngs::OsRng, Rng};
23use tinyjson::JsonValue;
24
25use crate::{
26    error::RpcError,
27    system::{Publisher, PublisherPtr},
28    Result,
29};
30
31/// JSON-RPC error codes.
32/// The error codes `[-32768, -32000]` are reserved for predefined errors.
33#[derive(Copy, Clone, Debug)]
34pub enum ErrorCode {
35    /// Invalid JSON was received by the server.
36    /// An error occurred on the server while parsing the JSON text.
37    ParseError,
38    /// The JSON sent is not a valid Request object.
39    InvalidRequest,
40    /// The method does not exist / is not available.
41    MethodNotFound,
42    /// Invalid method parameter(s).
43    InvalidParams,
44    /// Internal JSON-RPC error.
45    InternalError,
46    /// ID mismatch
47    IdMismatch,
48    /// Invalid/Unexpected reply
49    InvalidReply,
50    /// Reserved for implementation-defined server-errors.
51    ServerError(i32),
52}
53
54impl ErrorCode {
55    pub fn code(&self) -> i32 {
56        match *self {
57            Self::ParseError => -32700,
58            Self::InvalidRequest => -32600,
59            Self::MethodNotFound => -32601,
60            Self::InvalidParams => -32602,
61            Self::InternalError => -32603,
62            Self::IdMismatch => -32360,
63            Self::InvalidReply => -32361,
64            Self::ServerError(c) => c,
65        }
66    }
67
68    pub fn message(&self) -> String {
69        match *self {
70            Self::ParseError => "parse error".to_string(),
71            Self::InvalidRequest => "invalid request".to_string(),
72            Self::MethodNotFound => "method not found".to_string(),
73            Self::InvalidParams => "invalid params".to_string(),
74            Self::InternalError => "internal error".to_string(),
75            Self::IdMismatch => "id mismatch".to_string(),
76            Self::InvalidReply => "invalid reply".to_string(),
77            Self::ServerError(_) => "server error".to_string(),
78        }
79    }
80
81    pub fn desc(&self) -> JsonValue {
82        JsonValue::String(self.message())
83    }
84}
85
86// ANCHOR: jsonresult
87/// Wrapping enum around the available JSON-RPC object types
88#[derive(Clone, Debug)]
89pub enum JsonResult {
90    Response(JsonResponse),
91    Error(JsonError),
92    Notification(JsonNotification),
93    /// Subscriber is a special object that yields a channel
94    Subscriber(JsonSubscriber),
95    SubscriberWithReply(JsonSubscriber, JsonResponse),
96    Request(JsonRequest),
97}
98
99impl JsonResult {
100    pub fn try_from_value(value: &JsonValue) -> Result<Self> {
101        if let Ok(response) = JsonResponse::try_from(value) {
102            return Ok(Self::Response(response))
103        }
104
105        if let Ok(error) = JsonError::try_from(value) {
106            return Ok(Self::Error(error))
107        }
108
109        if let Ok(notification) = JsonNotification::try_from(value) {
110            return Ok(Self::Notification(notification))
111        }
112
113        Err(RpcError::InvalidJson("Invalid JSON Result".to_string()).into())
114    }
115}
116
117impl From<JsonResponse> for JsonResult {
118    fn from(resp: JsonResponse) -> Self {
119        Self::Response(resp)
120    }
121}
122
123impl From<JsonError> for JsonResult {
124    fn from(err: JsonError) -> Self {
125        Self::Error(err)
126    }
127}
128
129impl From<JsonNotification> for JsonResult {
130    fn from(notif: JsonNotification) -> Self {
131        Self::Notification(notif)
132    }
133}
134
135impl From<JsonSubscriber> for JsonResult {
136    fn from(sub: JsonSubscriber) -> Self {
137        Self::Subscriber(sub)
138    }
139}
140
141impl From<(JsonSubscriber, JsonResponse)> for JsonResult {
142    fn from(tuple: (JsonSubscriber, JsonResponse)) -> Self {
143        Self::SubscriberWithReply(tuple.0, tuple.1)
144    }
145}
146
147// ANCHOR: jsonrequest
148/// A JSON-RPC request object
149#[derive(Clone, Debug)]
150pub struct JsonRequest {
151    /// JSON-RPC version
152    pub jsonrpc: &'static str,
153    /// Request ID
154    pub id: u16,
155    /// Request method
156    pub method: String,
157    /// Request parameters
158    pub params: JsonValue,
159}
160// ANCHOR_END: jsonrequest
161
162impl JsonRequest {
163    /// Create a new [`JsonRequest`] object with the given method and parameters.
164    /// The request ID is chosen randomly.
165    pub fn new(method: &str, params: JsonValue) -> Self {
166        assert!(params.is_object() || params.is_array());
167        Self { jsonrpc: "2.0", id: OsRng::gen(&mut OsRng), method: method.to_string(), params }
168    }
169
170    /// Convert the object into a JSON string
171    pub fn stringify(&self) -> Result<String> {
172        let v: JsonValue = self.into();
173        Ok(v.stringify()?)
174    }
175}
176
177impl From<&JsonRequest> for JsonValue {
178    fn from(req: &JsonRequest) -> JsonValue {
179        JsonValue::Object(HashMap::from([
180            ("jsonrpc".to_string(), JsonValue::String(req.jsonrpc.to_string())),
181            ("id".to_string(), JsonValue::Number(req.id.into())),
182            ("method".to_string(), JsonValue::String(req.method.clone())),
183            ("params".to_string(), req.params.clone()),
184        ]))
185    }
186}
187
188impl TryFrom<&JsonValue> for JsonRequest {
189    type Error = RpcError;
190
191    fn try_from(value: &JsonValue) -> std::result::Result<Self, Self::Error> {
192        if !value.is_object() {
193            return Err(RpcError::InvalidJson("JSON is not an Object".to_string()))
194        }
195
196        // We have to allocate the value here another time in order to mutate
197        // it if necessary.
198        let mut value = value.clone();
199        let map: &mut HashMap<String, JsonValue> = value.get_mut().unwrap();
200
201        if !map.contains_key("jsonrpc") ||
202            !map["jsonrpc"].is_string() ||
203            map["jsonrpc"] != JsonValue::String("2.0".to_string())
204        {
205            return Err(RpcError::InvalidJson(
206                "Request does not contain valid \"jsonrpc\" field".to_string(),
207            ))
208        }
209
210        if !map.contains_key("id")
211        /* || !map["id"].is_number() */
212        {
213            return Err(RpcError::InvalidJson(
214                "Request does not contain valid \"id\" field".to_string(),
215            ))
216        }
217
218        if !map.contains_key("method") || !map["method"].is_string() {
219            return Err(RpcError::InvalidJson(
220                "Request does not contain valid \"method\" field".to_string(),
221            ))
222        }
223
224        if !map.contains_key("params") {
225            // HACK ALERT:
226            // On nonexisting `params`, we'll just make them into something.
227            map.insert("params".to_string(), JsonValue::from(vec![]));
228        }
229
230        if !map["params"].is_object() && !map["params"].is_array() {
231            return Err(RpcError::InvalidJson(
232                "Request does not contain valid \"params\" field".to_string(),
233            ))
234        }
235
236        // HACK ALERT:
237        // Some RPC clients send string IDs. We assume they're numeric, so
238        // here we cast the strings to numbers.
239        let id = if map["id"].is_number() {
240            *map["id"].get::<f64>().unwrap() as u16
241        } else if map["id"].is_string() {
242            match map["id"].get::<String>().unwrap().parse::<f64>() {
243                Ok(v) => v as u16,
244                Err(_) => {
245                    return Err(RpcError::InvalidJson(
246                        "Request does not contain valid \"id\" field".to_string(),
247                    ))
248                }
249            }
250        } else {
251            return Err(RpcError::InvalidJson(
252                "Request does not contain valid \"id\" field".to_string(),
253            ))
254        };
255
256        Ok(Self {
257            jsonrpc: "2.0",
258            id,
259            method: map["method"].get::<String>().unwrap().clone(),
260            params: map["params"].clone(),
261        })
262    }
263}
264
265/// A JSON-RPC notification object
266#[derive(Clone, Debug)]
267pub struct JsonNotification {
268    /// JSON-RPC version
269    pub jsonrpc: &'static str,
270    /// Notification method
271    pub method: String,
272    /// Notification parameters
273    pub params: JsonValue,
274}
275
276impl JsonNotification {
277    /// Create a new [`JsonNotification`] object with the given method and parameters.
278    pub fn new(method: &str, params: JsonValue) -> Self {
279        assert!(params.is_object() || params.is_array());
280        Self { jsonrpc: "2.0", method: method.to_string(), params }
281    }
282
283    /// Convert the object into a JSON string
284    pub fn stringify(&self) -> Result<String> {
285        let v: JsonValue = self.into();
286        Ok(v.stringify()?)
287    }
288}
289
290impl From<&JsonNotification> for JsonValue {
291    fn from(notif: &JsonNotification) -> JsonValue {
292        JsonValue::Object(HashMap::from([
293            ("jsonrpc".to_string(), JsonValue::String(notif.jsonrpc.to_string())),
294            ("method".to_string(), JsonValue::String(notif.method.clone())),
295            ("params".to_string(), notif.params.clone()),
296        ]))
297    }
298}
299
300impl TryFrom<&JsonValue> for JsonNotification {
301    type Error = RpcError;
302
303    fn try_from(value: &JsonValue) -> std::result::Result<Self, Self::Error> {
304        if !value.is_object() {
305            return Err(RpcError::InvalidJson("JSON is not an Object".to_string()))
306        }
307
308        let map: &HashMap<String, JsonValue> = value.get().unwrap();
309
310        if !map.contains_key("jsonrpc") ||
311            !map["jsonrpc"].is_string() ||
312            map["jsonrpc"] != JsonValue::String("2.0".to_string())
313        {
314            return Err(RpcError::InvalidJson(
315                "Notification does not contain valid \"jsonrpc\" field".to_string(),
316            ))
317        }
318
319        if !map.contains_key("method") || !map["method"].is_string() {
320            return Err(RpcError::InvalidJson(
321                "Notification does not contain valid \"method\" field".to_string(),
322            ))
323        }
324
325        if !map.contains_key("params") {
326            return Err(RpcError::InvalidJson(
327                "Notification does not contain valid \"params\" field".to_string(),
328            ))
329        }
330
331        if !map["params"].is_object() && !map["params"].is_array() {
332            return Err(RpcError::InvalidJson(
333                "Request does not contain valid \"params\" field".to_string(),
334            ))
335        }
336
337        Ok(Self {
338            jsonrpc: "2.0",
339            method: map["method"].get::<String>().unwrap().clone(),
340            params: map["params"].clone(),
341        })
342    }
343}
344
345/// A JSON-RPC response object
346#[derive(Clone, Debug)]
347pub struct JsonResponse {
348    /// JSON-RPC version
349    pub jsonrpc: &'static str,
350    /// Request ID
351    pub id: u16,
352    /// Response result
353    pub result: JsonValue,
354}
355
356impl JsonResponse {
357    /// Create a new [`JsonResponse`] object with the given ID and result value.
358    /// Creating a `JsonResponse` implies that the method call was successful.
359    pub fn new(result: JsonValue, id: u16) -> Self {
360        Self { jsonrpc: "2.0", id, result }
361    }
362
363    /// Convert the object into a JSON string
364    pub fn stringify(&self) -> Result<String> {
365        let v: JsonValue = self.into();
366        Ok(v.stringify()?)
367    }
368}
369
370impl From<&JsonResponse> for JsonValue {
371    fn from(rep: &JsonResponse) -> JsonValue {
372        JsonValue::Object(HashMap::from([
373            ("jsonrpc".to_string(), JsonValue::String(rep.jsonrpc.to_string())),
374            ("id".to_string(), JsonValue::Number(rep.id.into())),
375            ("result".to_string(), rep.result.clone()),
376        ]))
377    }
378}
379
380impl TryFrom<&JsonValue> for JsonResponse {
381    type Error = RpcError;
382
383    fn try_from(value: &JsonValue) -> std::result::Result<Self, Self::Error> {
384        if !value.is_object() {
385            return Err(RpcError::InvalidJson("Json is not an Object".to_string()))
386        }
387
388        let map: &HashMap<String, JsonValue> = value.get().unwrap();
389
390        if !map.contains_key("jsonrpc") ||
391            !map["jsonrpc"].is_string() ||
392            map["jsonrpc"] != JsonValue::String("2.0".to_string())
393        {
394            return Err(RpcError::InvalidJson(
395                "Response does not contain valid \"jsonrpc\" field".to_string(),
396            ))
397        }
398
399        if !map.contains_key("id") || !map["id"].is_number() {
400            return Err(RpcError::InvalidJson(
401                "Response does not contain valid \"id\" field".to_string(),
402            ))
403        }
404
405        if !map.contains_key("result") {
406            return Err(RpcError::InvalidJson(
407                "Response does not contain valid \"result\" field".to_string(),
408            ))
409        }
410
411        Ok(Self {
412            jsonrpc: "2.0",
413            id: *map["id"].get::<f64>().unwrap() as u16,
414            result: map["result"].clone(),
415        })
416    }
417}
418
419impl TryFrom<JsonResult> for JsonResponse {
420    type Error = RpcError;
421
422    /// Converts [`JsonResult`] to [`JsonResponse`], returning the response or an `InvalidJson`
423    /// error if the structure is not a `JsonResponse`.
424    fn try_from(result: JsonResult) -> std::result::Result<Self, Self::Error> {
425        match result {
426            JsonResult::Response(response) => Ok(response),
427            _ => Err(RpcError::InvalidJson("Not a JsonResult::Response".to_string())),
428        }
429    }
430}
431
432/// A JSON-RPC error object
433#[derive(Clone, Debug)]
434pub struct JsonError {
435    /// JSON-RPC version
436    pub jsonrpc: &'static str,
437    /// Request ID
438    pub id: u16,
439    /// JSON-RPC error (code and message)
440    pub error: JsonErrorVal,
441}
442
443/// A JSON-RPC error value (code and message)
444#[derive(Clone, Debug)]
445pub struct JsonErrorVal {
446    /// Error code
447    pub code: i32,
448    /// Error message
449    pub message: String,
450}
451
452impl JsonError {
453    /// Create a new [`JsonError`] object with the given error code, optional
454    /// message, and a response ID.
455    /// Creating a `JsonError` implies that the method call was unsuccessful.
456    pub fn new(c: ErrorCode, message: Option<String>, id: u16) -> Self {
457        let error = JsonErrorVal { code: c.code(), message: message.unwrap_or(c.message()) };
458        Self { jsonrpc: "2.0", id, error }
459    }
460
461    /// Convert the object into a JSON string
462    pub fn stringify(&self) -> Result<String> {
463        let v: JsonValue = self.into();
464        Ok(v.stringify()?)
465    }
466}
467
468impl From<&JsonError> for JsonValue {
469    fn from(err: &JsonError) -> JsonValue {
470        let errmap = JsonValue::Object(HashMap::from([
471            ("code".to_string(), JsonValue::Number(err.error.code.into())),
472            ("message".to_string(), JsonValue::String(err.error.message.clone())),
473        ]));
474
475        JsonValue::Object(HashMap::from([
476            ("jsonrpc".to_string(), JsonValue::String(err.jsonrpc.to_string())),
477            ("id".to_string(), JsonValue::Number(err.id.into())),
478            ("error".to_string(), errmap),
479        ]))
480    }
481}
482
483impl TryFrom<JsonResult> for JsonError {
484    type Error = RpcError;
485
486    /// Converts [`JsonResult`] to [`JsonError`], returning the response or an `InvalidJson`
487    /// error if the structure is not a `JsonError`.
488    fn try_from(result: JsonResult) -> std::result::Result<Self, Self::Error> {
489        match result {
490            JsonResult::Error(error) => Ok(error),
491            _ => Err(RpcError::InvalidJson("Not a JsonResult::Error".to_string())),
492        }
493    }
494}
495
496impl TryFrom<&JsonValue> for JsonError {
497    type Error = RpcError;
498
499    fn try_from(value: &JsonValue) -> std::result::Result<Self, Self::Error> {
500        if !value.is_object() {
501            return Err(RpcError::InvalidJson("JSON is not an Object".to_string()))
502        }
503
504        let map: &HashMap<String, JsonValue> = value.get().unwrap();
505
506        if !map.contains_key("jsonrpc") ||
507            !map["jsonrpc"].is_string() ||
508            map["jsonrpc"] != JsonValue::String("2.0".to_string())
509        {
510            return Err(RpcError::InvalidJson(
511                "Error does not contain valid \"jsonrpc\" field".to_string(),
512            ))
513        }
514
515        if !map.contains_key("id") || !map["id"].is_number() {
516            return Err(RpcError::InvalidJson(
517                "Error does not contain valid \"id\" field".to_string(),
518            ))
519        }
520
521        if !map.contains_key("error") || !map["error"].is_object() {
522            return Err(RpcError::InvalidJson(
523                "Error does not contain valid \"error\" field".to_string(),
524            ))
525        }
526
527        if !map["error"]["code"].is_number() {
528            return Err(RpcError::InvalidJson(
529                "Error does not contain valid \"error.code\" field".to_string(),
530            ))
531        }
532
533        if !map["error"]["message"].is_string() {
534            return Err(RpcError::InvalidJson(
535                "Error does not contain valid \"error.message\" field".to_string(),
536            ))
537        }
538
539        Ok(Self {
540            jsonrpc: "2.0",
541            id: *map["id"].get::<f64>().unwrap() as u16,
542            error: JsonErrorVal {
543                code: *map["error"]["code"].get::<f64>().unwrap() as i32,
544                message: map["error"]["message"].get::<String>().unwrap().to_string(),
545            },
546        })
547    }
548}
549
550/// A JSON-RPC subscriber for notifications
551#[derive(Clone, Debug)]
552pub struct JsonSubscriber {
553    /// Notification method
554    pub method: &'static str,
555    /// Notification publisher
556    pub publisher: PublisherPtr<JsonNotification>,
557}
558
559impl JsonSubscriber {
560    pub fn new(method: &'static str) -> Self {
561        let publisher = Publisher::new();
562        Self { method, publisher }
563    }
564
565    /// Send a notification to the publisher with the given JSON object
566    pub async fn notify(&self, params: JsonValue) {
567        let notification = JsonNotification::new(self.method, params);
568        self.publisher.notify(notification).await;
569    }
570}
571
572/// Parses a [`JsonValue`] parameter into a `String`.
573/// Returns the string if successful or an error if the value is not a valid string.
574pub fn parse_json_string(name: &str, value: &JsonValue) -> std::result::Result<String, RpcError> {
575    value
576        .get::<String>()
577        .cloned()
578        .ok_or_else(|| RpcError::InvalidJson(format!("Parameter '{name}' is not a valid string")))
579}
580
581/// Parses a [`JsonValue`] parameter into a `f64`.
582/// Returns the number if successful or an error if the value is not a valid number.
583pub fn parse_json_number(name: &str, value: &JsonValue) -> std::result::Result<f64, RpcError> {
584    value.get::<f64>().cloned().ok_or_else(|| {
585        RpcError::InvalidJson(format!("Parameter '{name}' is not a supported number type"))
586    })
587}
588
589/// Parses the element at the specified index in a [`JsonValue::Array`] into a
590/// string. Returns the string if successful, or an error if the parameter is
591/// missing, not an array, or not a valid string.
592pub fn parse_json_array_string(
593    name: &str,
594    index: usize,
595    array_value: &JsonValue,
596) -> std::result::Result<String, RpcError> {
597    match array_value {
598        JsonValue::Array(values) => values
599            .get(index)
600            .ok_or_else(|| {
601                RpcError::InvalidJson(format!("Parameter '{name}' at index {index} is missing"))
602            })
603            .and_then(|param| parse_json_string(name, param)),
604        _ => Err(RpcError::InvalidJson(format!("Parameter '{name}' is not an array"))),
605    }
606}
607
608/// Parses the element at the specified index in a [`JsonValue::Array`] into an
609/// `f64` (compatible with [`JsonValue::Number`]). Returns the number if successful,
610/// or an error if the parameter is missing, not an array, or is not a valid number.
611pub fn parse_json_array_number(
612    name: &str,
613    index: usize,
614    array_value: &JsonValue,
615) -> std::result::Result<f64, RpcError> {
616    match array_value {
617        JsonValue::Array(values) => values
618            .get(index)
619            .ok_or_else(|| {
620                RpcError::InvalidJson(format!("Parameter '{name}' at index {index} is missing"))
621            })
622            .and_then(|param| parse_json_number(name, param)),
623        _ => Err(RpcError::InvalidJson(format!("Parameter '{name}' is not an array"))),
624    }
625}
626
627/// Attempts to parse a `JsonResult`, converting it into a `JsonResponse` and
628/// extracting a string result from it. Returns an error if conversion or
629/// extraction fails, and the extracted string on success.
630pub fn parse_json_response_string(
631    json_result: JsonResult,
632) -> std::result::Result<String, RpcError> {
633    // Try converting `JsonResult` into a `JsonResponse`.
634    let json_response: JsonResponse = json_result.try_into().map_err(|_| {
635        RpcError::InvalidJson("Failed to convert JsonResult into JsonResponse".to_string())
636    })?;
637
638    // Attempt to extract a string result from the JsonResponse
639    json_response.result.get::<String>().map(|value| value.to_string()).ok_or_else(|| {
640        RpcError::InvalidJson("Failed to parse string from JsonResponse result".to_string())
641    })
642}
643
644/// Converts the provided JSON-RPC parameters into an array of JSON values,
645/// returning a reference to the array if successful, or a JsonResult error containing a
646/// JsonError when the input is not a JSON array.
647pub fn to_json_array(params: &JsonValue) -> std::result::Result<&Vec<JsonValue>, RpcError> {
648    if let JsonValue::Array(array) = params {
649        Ok(array)
650    } else {
651        Err(RpcError::InvalidJson(
652            "Expected an array of values, but received a different JSON type.".to_string(),
653        ))
654    }
655}
656
657/// 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.
658pub fn validate_empty_params(params: &JsonValue) -> std::result::Result<(), RpcError> {
659    match to_json_array(params) {
660        Ok(array) if array.is_empty() => Ok(()),
661        Ok(_) => Err(RpcError::InvalidJson(format!(
662            "Parameters not permited, received: {:?}",
663            params.stringify().unwrap_or("Error converting JSON to string".to_string())
664        ))),
665        Err(err) => Err(RpcError::InvalidJson(err.to_string())),
666    }
667}