/ bin / explorer / explorerd / src / rpc / blocks.rs
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  }