explorerd/rpc/
blocks.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 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
19use tinyjson::JsonValue;
20
21use darkfi::{
22    blockchain::BlockInfo,
23    error::RpcError,
24    rpc::jsonrpc::{parse_json_array_number, parse_json_array_string},
25    util::encoding::base64,
26    Result,
27};
28use darkfi_serial::deserialize_async;
29
30use crate::{rpc::DarkfidRpcClient, Explorerd};
31
32impl DarkfidRpcClient {
33    /// Retrieves a block from at a given height returning the corresponding [`BlockInfo`].
34    pub async fn get_block_by_height(&self, height: u32) -> Result<BlockInfo> {
35        let params = self
36            .request(
37                "blockchain.get_block",
38                &JsonValue::Array(vec![JsonValue::String(height.to_string())]),
39            )
40            .await?;
41        let param = params.get::<String>().unwrap();
42        let bytes = base64::decode(param).unwrap();
43        let block = deserialize_async(&bytes).await?;
44        Ok(block)
45    }
46
47    /// Retrieves the last confirmed block returning the block height and its header hash.
48    pub async fn get_last_confirmed_block(&self) -> Result<(u32, String)> {
49        let rep =
50            self.request("blockchain.last_confirmed_block", &JsonValue::Array(vec![])).await?;
51        let params = rep.get::<Vec<JsonValue>>().unwrap();
52        let height = *params[0].get::<f64>().unwrap() as u32;
53        let hash = params[1].get::<String>().unwrap().clone();
54
55        Ok((height, hash))
56    }
57}
58
59impl Explorerd {
60    // RPCAPI:
61    // Queries the database to retrieve last N blocks.
62    // Returns an array of readable blocks upon success.
63    //
64    // **Params:**
65    // * `array[0]`: `u16` Number of blocks to retrieve (as string)
66    //
67    // **Returns:**
68    // * Array of `BlockRecord` encoded into a JSON.
69    //
70    // **Example API Usage:**
71    // --> {"jsonrpc": "2.0", "method": "blocks.get_last_n_blocks", "params": [10], "id": 1}
72    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
73    pub async fn blocks_get_last_n_blocks(&self, params: &JsonValue) -> Result<JsonValue> {
74        // Extract the number of last blocks to fetch
75        let num_last_blocks = parse_json_array_number("num_last_blocks", 0, params)? as usize;
76
77        // Fetch the blocks
78        let blocks_result = self.service.get_last_n(num_last_blocks)?;
79
80        // Transform blocks to `JsonValue`
81        if blocks_result.is_empty() {
82            Ok(JsonValue::Array(vec![]))
83        } else {
84            let json_blocks: Vec<JsonValue> =
85                blocks_result.into_iter().map(|block| block.to_json_array()).collect();
86            Ok(JsonValue::Array(json_blocks))
87        }
88    }
89
90    // RPCAPI:
91    // Queries the database to retrieve blocks in provided heights range.
92    // Returns an array of readable blocks upon success.
93    //
94    // **Params:**
95    // * `array[0]`: `u32` Starting height (as string)
96    // * `array[1]`: `u32` Ending height range (as string)
97    //
98    // **Returns:**
99    // * Array of `BlockRecord` encoded into a JSON.
100    //
101    // **Example API Usage:**
102    // --> {"jsonrpc": "2.0", "method": "blocks.get_blocks_in_heights_range", "params": [10, 15], "id": 1}
103    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
104    pub async fn blocks_get_blocks_in_heights_range(
105        &self,
106        params: &JsonValue,
107    ) -> Result<JsonValue> {
108        // Extract the start range
109        let start = parse_json_array_number("start", 0, params)? as u32;
110
111        // Extract the end range
112        let end = parse_json_array_number("end", 1, params)? as u32;
113
114        // Validate for valid range
115        if start > end {
116            return Err(RpcError::InvalidJson(format!(
117                "Invalid range: start ({start}) cannot be greater than end ({end})"
118            ))
119            .into());
120        }
121
122        // Fetch the blocks
123        let blocks_result = self.service.get_by_range(start, end)?;
124
125        // Transform blocks to `JsonValue` and return result
126        if blocks_result.is_empty() {
127            Ok(JsonValue::Array(vec![]))
128        } else {
129            let json_blocks: Vec<JsonValue> =
130                blocks_result.into_iter().map(|block| block.to_json_array()).collect();
131            Ok(JsonValue::Array(json_blocks))
132        }
133    }
134
135    // RPCAPI:
136    // Queries the database to retrieve the block corresponding to the provided hash.
137    // Returns the readable block upon success.
138    //
139    // **Params:**
140    // * `array[0]`: `String` Block header hash
141    //
142    // **Returns:**
143    // * `BlockRecord` encoded into a JSON.
144    //
145    // **Example API Usage:**
146    // --> {"jsonrpc": "2.0", "method": "blocks.get_block_by_hash", "params": ["5cc...2f9"], "id": 1}
147    // <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
148    pub async fn blocks_get_block_by_hash(&self, params: &JsonValue) -> Result<JsonValue> {
149        // Extract header hash
150        let header_hash = parse_json_array_string("header_hash", 0, params)?;
151
152        // Fetch and transform block to `JsonValue`
153        match self.service.get_block_by_hash(&header_hash)? {
154            Some(block) => Ok(block.to_json_array()),
155            None => Ok(JsonValue::Array(vec![])),
156        }
157    }
158}
159
160#[cfg(test)]
161/// Test module for validating the functionality of RPC methods related to explorer blocks.
162/// Focuses on ensuring proper error handling for invalid parameters across several use cases,
163/// including cases with missing values, unsupported types, invalid ranges, and unparsable inputs.
164mod tests {
165
166    use tinyjson::JsonValue;
167
168    use darkfi::rpc::{
169        jsonrpc::{ErrorCode, JsonRequest, JsonResult},
170        server::RequestHandler,
171    };
172
173    use crate::test_utils::{
174        setup, validate_invalid_rpc_header_hash, validate_invalid_rpc_parameter,
175    };
176
177    #[test]
178    /// Tests the handling of invalid parameters for the `blocks.get_last_n_blocks` JSON-RPC method.
179    /// Verifies that missing and an invalid `num_last_blocks` value results in an appropriate error.
180    fn test_blocks_get_last_n_blocks_invalid_params() {
181        smol::block_on(async {
182            // Define rpc_method and parameter names
183            let rpc_method = "blocks.get_last_n_blocks";
184            let parameter_name = "num_last_blocks";
185
186            // Set up the Explorerd instance
187            let explorerd = setup();
188
189            // Test for missing `start` parameter
190            validate_invalid_rpc_parameter(
191                &explorerd,
192                rpc_method,
193                &[],
194                ErrorCode::InvalidParams.code(),
195                &format!("Parameter '{parameter_name}' at index 0 is missing"),
196            )
197            .await;
198
199            // Test for invalid num_last_blocks parameter
200            validate_invalid_rpc_parameter(
201                &explorerd,
202                rpc_method,
203                &[JsonValue::String("invalid_number".to_string())],
204                ErrorCode::InvalidParams.code(),
205                &format!("Parameter '{parameter_name}' is not a supported number type"),
206            )
207            .await;
208        });
209    }
210
211    #[test]
212    /// Tests the handling of invalid parameters for the `blocks.get_blocks_in_heights_range`
213    /// JSON-RPC method. Verifies that invalid/missing `start` or `end` parameter values, or an
214    /// invalid range where `start` is greater than `end`, result in appropriate errors.
215    fn test_blocks_get_blocks_in_heights_range_invalid_params() {
216        smol::block_on(async {
217            // Define rpc_method and parameter names
218            let rpc_method = "blocks.get_blocks_in_heights_range";
219            let start_parameter_name = "start";
220            let end_parameter_name = "end";
221
222            // Set up the Explorerd instance
223            let explorerd = setup();
224
225            // Test for missing `start` parameter
226            validate_invalid_rpc_parameter(
227                &explorerd,
228                rpc_method,
229                &[],
230                ErrorCode::InvalidParams.code(),
231                &format!("Parameter '{start_parameter_name}' at index 0 is missing"),
232            )
233            .await;
234
235            // Test for invalid `start` parameter
236            validate_invalid_rpc_parameter(
237                &explorerd,
238                rpc_method,
239                &[JsonValue::String("invalid_number".to_string()), JsonValue::Number(10.0)],
240                ErrorCode::InvalidParams.code(),
241                &format!("Parameter '{start_parameter_name}' is not a supported number type"),
242            )
243            .await;
244
245            // Test for invalid `end` parameter
246            validate_invalid_rpc_parameter(
247                &explorerd,
248                rpc_method,
249                &[JsonValue::Number(10.0)],
250                ErrorCode::InvalidParams.code(),
251                &format!("Parameter '{end_parameter_name}' at index 1 is missing"),
252            )
253            .await;
254
255            // Test for invalid `end` parameter
256            validate_invalid_rpc_parameter(
257                &explorerd,
258                rpc_method,
259                &[JsonValue::Number(10.0), JsonValue::String("invalid_number".to_string())],
260                ErrorCode::InvalidParams.code(),
261                &format!("Parameter '{end_parameter_name}' is not a supported number type"),
262            )
263            .await;
264
265            // Test invalid range where `start` > `end`
266            let request = JsonRequest {
267                id: 1,
268                jsonrpc: "2.0",
269                method: rpc_method.to_string(),
270                params: JsonValue::Array(vec![JsonValue::Number(20.0), JsonValue::Number(10.0)]),
271            };
272
273            let response = explorerd.handle_request(request).await;
274
275            // Verify that `start > end` error is raised
276            match response {
277                JsonResult::Error(actual_error) => {
278                    let expected_error_code = ErrorCode::InvalidParams.code();
279                    assert_eq!(
280                        actual_error.error.code,
281                        expected_error_code
282                    );
283                    assert_eq!(
284                        actual_error.error.message,
285                        "Invalid range: start (20) cannot be greater than end (10)"
286                    );
287                }
288                _ => panic!(
289                    "Expected a JSON error response for method: {rpc_method}, but got something else",
290                ),
291            }
292        });
293    }
294    #[test]
295    /// Tests the handling of invalid parameters for the `blocks.get_block_by_hash` JSON-RPC method.
296    /// Verifies that an invalid `header_hash` value, either a numeric type or invalid hash string,
297    /// results in appropriate error.
298    fn test_blocks_get_block_by_hash_invalid_params() {
299        smol::block_on(async {
300            // Define the RPC method name
301            let rpc_method = "blocks.get_block_by_hash";
302
303            // Set up the explorerd
304            let explorerd = setup();
305
306            // Validate when provided with an invalid tx hash
307            validate_invalid_rpc_header_hash(&explorerd, rpc_method);
308        });
309    }
310}