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}