/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2025 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use tinyjson::JsonValue;
use darkfi::{
blockchain::BlockInfo,
error::RpcError,
rpc::jsonrpc::{parse_json_array_number, parse_json_array_string},
util::encoding::base64,
Result,
};
use darkfi_serial::deserialize_async;
use crate::{rpc::DarkfidRpcClient, Explorerd};
impl DarkfidRpcClient {
/// Retrieves a block from at a given height returning the corresponding [`BlockInfo`].
pub async fn get_block_by_height(&self, height: u32) -> Result {
let params = self
.request(
"blockchain.get_block",
&JsonValue::Array(vec![JsonValue::String(height.to_string())]),
)
.await?;
let param = params.get::().unwrap();
let bytes = base64::decode(param).unwrap();
let block = deserialize_async(&bytes).await?;
Ok(block)
}
/// Retrieves the last confirmed block returning the block height and its header hash.
pub async fn get_last_confirmed_block(&self) -> Result<(u32, String)> {
let rep =
self.request("blockchain.last_confirmed_block", &JsonValue::Array(vec![])).await?;
let params = rep.get::>().unwrap();
let height = *params[0].get::().unwrap() as u32;
let hash = params[1].get::().unwrap().clone();
Ok((height, hash))
}
}
impl Explorerd {
// RPCAPI:
// Queries the database to retrieve last N blocks.
// Returns an array of readable blocks upon success.
//
// **Params:**
// * `array[0]`: `u16` Number of blocks to retrieve (as string)
//
// **Returns:**
// * Array of `BlockRecord` encoded into a JSON.
//
// **Example API Usage:**
// --> {"jsonrpc": "2.0", "method": "blocks.get_last_n_blocks", "params": [10], "id": 1}
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
pub async fn blocks_get_last_n_blocks(&self, params: &JsonValue) -> Result {
// Extract the number of last blocks to fetch
let num_last_blocks = parse_json_array_number("num_last_blocks", 0, params)? as usize;
// Fetch the blocks
let blocks_result = self.service.get_last_n(num_last_blocks)?;
// Transform blocks to `JsonValue`
if blocks_result.is_empty() {
Ok(JsonValue::Array(vec![]))
} else {
let json_blocks: Vec =
blocks_result.into_iter().map(|block| block.to_json_array()).collect();
Ok(JsonValue::Array(json_blocks))
}
}
// RPCAPI:
// Queries the database to retrieve blocks in provided heights range.
// Returns an array of readable blocks upon success.
//
// **Params:**
// * `array[0]`: `u32` Starting height (as string)
// * `array[1]`: `u32` Ending height range (as string)
//
// **Returns:**
// * Array of `BlockRecord` encoded into a JSON.
//
// **Example API Usage:**
// --> {"jsonrpc": "2.0", "method": "blocks.get_blocks_in_heights_range", "params": [10, 15], "id": 1}
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
pub async fn blocks_get_blocks_in_heights_range(
&self,
params: &JsonValue,
) -> Result {
// Extract the start range
let start = parse_json_array_number("start", 0, params)? as u32;
// Extract the end range
let end = parse_json_array_number("end", 1, params)? as u32;
// Validate for valid range
if start > end {
return Err(RpcError::InvalidJson(format!(
"Invalid range: start ({start}) cannot be greater than end ({end})"
))
.into());
}
// Fetch the blocks
let blocks_result = self.service.get_by_range(start, end)?;
// Transform blocks to `JsonValue` and return result
if blocks_result.is_empty() {
Ok(JsonValue::Array(vec![]))
} else {
let json_blocks: Vec =
blocks_result.into_iter().map(|block| block.to_json_array()).collect();
Ok(JsonValue::Array(json_blocks))
}
}
// RPCAPI:
// Queries the database to retrieve the block corresponding to the provided hash.
// Returns the readable block upon success.
//
// **Params:**
// * `array[0]`: `String` Block header hash
//
// **Returns:**
// * `BlockRecord` encoded into a JSON.
//
// **Example API Usage:**
// --> {"jsonrpc": "2.0", "method": "blocks.get_block_by_hash", "params": ["5cc...2f9"], "id": 1}
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
pub async fn blocks_get_block_by_hash(&self, params: &JsonValue) -> Result {
// Extract header hash
let header_hash = parse_json_array_string("header_hash", 0, params)?;
// Fetch and transform block to `JsonValue`
match self.service.get_block_by_hash(&header_hash)? {
Some(block) => Ok(block.to_json_array()),
None => Ok(JsonValue::Array(vec![])),
}
}
}
#[cfg(test)]
/// Test module for validating the functionality of RPC methods related to explorer blocks.
/// Focuses on ensuring proper error handling for invalid parameters across several use cases,
/// including cases with missing values, unsupported types, invalid ranges, and unparsable inputs.
mod tests {
use tinyjson::JsonValue;
use darkfi::rpc::{
jsonrpc::{ErrorCode, JsonRequest, JsonResult},
server::RequestHandler,
};
use crate::test_utils::{
setup, validate_invalid_rpc_header_hash, validate_invalid_rpc_parameter,
};
#[test]
/// Tests the handling of invalid parameters for the `blocks.get_last_n_blocks` JSON-RPC method.
/// Verifies that missing and an invalid `num_last_blocks` value results in an appropriate error.
fn test_blocks_get_last_n_blocks_invalid_params() {
smol::block_on(async {
// Define rpc_method and parameter names
let rpc_method = "blocks.get_last_n_blocks";
let parameter_name = "num_last_blocks";
// Set up the Explorerd instance
let explorerd = setup();
// Test for missing `start` parameter
validate_invalid_rpc_parameter(
&explorerd,
rpc_method,
&[],
ErrorCode::InvalidParams.code(),
&format!("Parameter '{}' at index 0 is missing", parameter_name),
)
.await;
// Test for invalid num_last_blocks parameter
validate_invalid_rpc_parameter(
&explorerd,
rpc_method,
&[JsonValue::String("invalid_number".to_string())],
ErrorCode::InvalidParams.code(),
&format!("Parameter '{}' is not a supported number type", parameter_name),
)
.await;
});
}
#[test]
/// Tests the handling of invalid parameters for the `blocks.get_blocks_in_heights_range`
/// JSON-RPC method. Verifies that invalid/missing `start` or `end` parameter values, or an
/// invalid range where `start` is greater than `end`, result in appropriate errors.
fn test_blocks_get_blocks_in_heights_range_invalid_params() {
smol::block_on(async {
// Define rpc_method and parameter names
let rpc_method = "blocks.get_blocks_in_heights_range";
let start_parameter_name = "start";
let end_parameter_name = "end";
// Set up the Explorerd instance
let explorerd = setup();
// Test for missing `start` parameter
validate_invalid_rpc_parameter(
&explorerd,
rpc_method,
&[],
ErrorCode::InvalidParams.code(),
&format!("Parameter '{}' at index 0 is missing", start_parameter_name),
)
.await;
// Test for invalid `start` parameter
validate_invalid_rpc_parameter(
&explorerd,
rpc_method,
&[JsonValue::String("invalid_number".to_string()), JsonValue::Number(10.0)],
ErrorCode::InvalidParams.code(),
&format!("Parameter '{start_parameter_name}' is not a supported number type"),
)
.await;
// Test for invalid `end` parameter
validate_invalid_rpc_parameter(
&explorerd,
rpc_method,
&[JsonValue::Number(10.0)],
ErrorCode::InvalidParams.code(),
&format!("Parameter '{}' at index 1 is missing", end_parameter_name),
)
.await;
// Test for invalid `end` parameter
validate_invalid_rpc_parameter(
&explorerd,
rpc_method,
&[JsonValue::Number(10.0), JsonValue::String("invalid_number".to_string())],
ErrorCode::InvalidParams.code(),
&format!("Parameter '{end_parameter_name}' is not a supported number type"),
)
.await;
// Test invalid range where `start` > `end`
let request = JsonRequest {
id: 1,
jsonrpc: "2.0",
method: rpc_method.to_string(),
params: JsonValue::Array(vec![JsonValue::Number(20.0), JsonValue::Number(10.0)]),
};
let response = explorerd.handle_request(request).await;
// Verify that `start > end` error is raised
match response {
JsonResult::Error(actual_error) => {
let expected_error_code = ErrorCode::InvalidParams.code();
assert_eq!(
actual_error.error.code,
expected_error_code
);
assert_eq!(
actual_error.error.message,
"Invalid range: start (20) cannot be greater than end (10)"
);
}
_ => panic!(
"Expected a JSON error response for method: {rpc_method}, but got something else",
),
}
});
}
#[test]
/// Tests the handling of invalid parameters for the `blocks.get_block_by_hash` JSON-RPC method.
/// Verifies that an invalid `header_hash` value, either a numeric type or invalid hash string,
/// results in appropriate error.
fn test_blocks_get_block_by_hash_invalid_params() {
smol::block_on(async {
// Define the RPC method name
let rpc_method = "blocks.get_block_by_hash";
// Set up the explorerd
let explorerd = setup();
// Validate when provided with an invalid tx hash
validate_invalid_rpc_header_hash(&explorerd, rpc_method);
});
}
}