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 19 use tinyjson::JsonValue; 20 21 use 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 }; 28 use darkfi_serial::deserialize_async; 29 30 use crate::{rpc::DarkfidRpcClient, Explorerd}; 31 32 impl 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 59 impl 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. 164 mod 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 }