blocks.rs 11 KB

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