diff --git a/src/api/ApiServer.cpp b/src/api/ApiServer.cpp index 6dcdfd9f..e8dbb0fd 100644 --- a/src/api/ApiServer.cpp +++ b/src/api/ApiServer.cpp @@ -16,8 +16,7 @@ #include "controller/ApiCoreInfo.hpp" #include "controller/path/info.hpp" #include "controller/path/block.hpp" -#include "controller/path/block/hash.hpp" -#include "controller/path/block/id.hpp" +#include "controller/path/block/identifier.hpp" #include "controller/path/info/version.hpp" #include "oatpp/network/Server.hpp" @@ -82,14 +81,9 @@ void ApiServer::run() { docEndpoints->append(blockController->getEndpoints()); router->addController(blockController); - auto blockByHashController = std::make_shared(); - docEndpoints->append(blockByHashController->getEndpoints()); - router->addController(blockByHashController); - - auto blockByIdController = std::make_shared(); - docEndpoints->append(blockByIdController->getEndpoints()); - router->addController(blockByIdController); - + auto blockIdentifierController = std::make_shared(); + docEndpoints->append(blockIdentifierController->getEndpoints()); + router->addController(blockIdentifierController); OATPP_CREATE_COMPONENT(std::shared_ptr, swaggerDocumentInfo) ([] diff --git a/src/api/controller/path/block.hpp b/src/api/controller/path/block.hpp index c95d947b..3f0b5211 100644 --- a/src/api/controller/path/block.hpp +++ b/src/api/controller/path/block.hpp @@ -24,61 +24,21 @@ #include "oatpp/parser/json/mapping/ObjectMapper.hpp" #include "rpc/core_rpc_server_commands_defs.h" -#include // For std::string and std::stoull +#include #include OATPP_CODEGEN_BEGIN(ApiController) /** * Block Controller - * Acts as a proxy to fetch blocks by hash or by height (ID). + * Retrieves one or more blocks with optional pagination. */ class BlockController : public oatpp::web::server::api::ApiController { private: OATPP_COMPONENT(std::shared_ptr, m_core_info); -public: - explicit BlockController(OATPP_COMPONENT(std::shared_ptr, objectMapper)) - : oatpp::web::server::api::ApiController(objectMapper) - {} -public: - ENDPOINT_INFO(getBlock) { - info->summary = "Get a block by its hash or height (ID)"; - info->addTag("Block"); - info->pathParams["identifier"].description = "The hash (hex string) or height (integer) of the block to retrieve."; - info->addResponse>(Status::CODE_200, "application/json"); - info->addResponse(Status::CODE_404, "text/plain"); - info->addResponse(Status::CODE_400, "text/plain"); - } - ENDPOINT("GET", "/block/{identifier}", getBlock, PATH(String, identifier)) { - - currency::block_rpc_extended_info rpc_details; - bool block_found = false; - - // Check if the identifier consists only of digits - if (identifier->find_first_not_of("0123456789") == std::string::npos) { - // It's a numeric ID (height) - try { - uint64_t height = std::stoull(identifier->c_str()); - block_found = m_core_info->getCore().get_blockchain_storage().get_main_block_rpc_details(height, rpc_details); - } catch (const std::exception& e) { - return createResponse(Status::CODE_400, "Invalid block height format"); - } - } else { - // It's a hash - crypto::hash block_hash{}; - if (!epee::string_tools::hex_to_pod(*identifier, block_hash)) { - return createResponse(Status::CODE_400, "Invalid block hash format"); - } - block_found = m_core_info->getCore().get_blockchain_storage().get_main_block_rpc_details(block_hash, rpc_details); - } - - if (!block_found) { - return createResponse(Status::CODE_404, "Block not found"); - } - - // Common logic to populate the DTO + // Helper function to populate a block details model from RPC details + oatpp::Object populateBlockDetailsModel(const currency::block_rpc_extended_info& rpc_details) { auto blockDetails = BlockDetailsModel::createShared(); - blockDetails->id = rpc_details.id; blockDetails->height = rpc_details.height; blockDetails->timestamp = rpc_details.timestamp; @@ -111,8 +71,99 @@ public: tx_details_list->push_back(tx_model); } blockDetails->transactions_details = tx_details_list; + return blockDetails; + } - return createDtoResponse(Status::CODE_200, blockDetails); +public: + explicit BlockController(OATPP_COMPONENT(std::shared_ptr, objectMapper)) + : oatpp::web::server::api::ApiController(objectMapper) + {} +public: + + ENDPOINT_INFO(getBlocks) { + info->summary = "Get one or more blocks, with optional pagination."; + info->addTag("Block"); + info->queryParams["limit"].description = "Number of blocks to retrieve. Default is 1. If limit is 1, a single block object is returned. Otherwise, a list of blocks is returned."; + info->queryParams["offset"].description = "Number of blocks to skip from the start height. Default is 0."; + info->queryParams["start"].description = "The starting block height. If not provided, the current top block height is used."; + info->addResponse>(Status::CODE_200, "application/json", "A single block object."); + info->addResponse>>(Status::CODE_200, "application/json", "A list of block objects."); + info->addResponse(Status::CODE_404, "text/plain"); + info->addResponse(Status::CODE_400, "text/plain"); + } + ENDPOINT("GET", "/block", getBlocks, QUERIES(QueryParams, queryParams)) { + const auto limitStr = queryParams.get("limit"); + const auto offsetStr = queryParams.get("offset"); + const auto startStr = queryParams.get("start"); + + uint64_t limit = 1; + if (limitStr) { + try { + limit = std::stoull(limitStr->c_str()); + } catch (const std::exception& e) { + return createResponse(Status::CODE_400, "Invalid 'limit' parameter"); + } + } + + uint64_t offset = 0; + if (offsetStr) { + try { + offset = std::stoull(offsetStr->c_str()); + } catch (const std::exception& e) { + return createResponse(Status::CODE_400, "Invalid 'offset' parameter"); + } + } + + uint64_t start_height; + if (startStr) { + try { + start_height = std::stoull(startStr->c_str()); + } catch (const std::exception& e) { + return createResponse(Status::CODE_400, "Invalid 'start' parameter"); + } + } else { + start_height = m_core_info->getCore().get_blockchain_storage().get_current_blockchain_size() - 1; + } + + if (limit == 0) { + return createResponse(Status::CODE_400, "'limit' must be greater than 0"); + } + + if (start_height < offset) { + return createResponse(Status::CODE_400, "'start' height cannot be less than 'offset'"); + } + + uint64_t current_height = start_height - offset; + + if (limit == 1) { + currency::block_rpc_extended_info rpc_details; + if (!m_core_info->getCore().get_blockchain_storage().get_main_block_rpc_details(current_height, rpc_details)) { + return createResponse(Status::CODE_404, "Block not found at specified height"); + } + return createDtoResponse(Status::CODE_200, populateBlockDetailsModel(rpc_details)); + } + + auto block_list = oatpp::List>::createShared(); + for(uint64_t i = 0; i < limit; ++i) + { + if(current_height < i) + { + break; // Reached genesis + } + uint64_t height_to_fetch = current_height - i; + currency::block_rpc_extended_info rpc_details; + if(m_core_info->getCore().get_blockchain_storage().get_main_block_rpc_details(height_to_fetch, rpc_details)) + { + block_list->push_back(populateBlockDetailsModel(rpc_details)); + } + else + { + // Could be that we requested past genesis, or a block is missing for some reason. + // We'll just stop here. + break; + } + } + return createDtoResponse(Status::CODE_200, block_list); } }; diff --git a/src/api/controller/path/block/identifier.hpp b/src/api/controller/path/block/identifier.hpp new file mode 100644 index 00000000..de423cdc --- /dev/null +++ b/src/api/controller/path/block/identifier.hpp @@ -0,0 +1,122 @@ +// Copyright (c) 2017-2025 Lethean (https://lt.hn) +// +// Licensed under the European Union Public Licence (EUPL) version 1.2. +// You may obtain a copy of the licence at: +// +// https://joinup.ec.europa.eu/software/page/eupl/licence-eupl +// +// The EUPL is a copyleft licence that is compatible with the MIT/X11 +// licence used by the original projects; but maintains OSS status, +// where regional copyright law requires ownership to dictate licence terms. +// +// SPDX‑License‑Identifier: EUPL-1.2 +// + +#ifndef BlockIdentifierController_hpp +#define BlockIdentifierController_hpp + +#include "modal/block/details.hpp" +#include "modal/transaction/details.hpp" +#include "controller/ApiCoreInfo.hpp" + +#include "oatpp/web/server/api/ApiController.hpp" +#include "oatpp/core/macro/codegen.hpp" +#include "oatpp/parser/json/mapping/ObjectMapper.hpp" + +#include "rpc/core_rpc_server_commands_defs.h" +#include // For std::string and std::stoull + +#include OATPP_CODEGEN_BEGIN(ApiController) + +/** + * Block Identifier Controller + * Acts as a proxy to fetch blocks by hash or by height (ID). + */ +class BlockIdentifierController : public oatpp::web::server::api::ApiController { +private: + OATPP_COMPONENT(std::shared_ptr, m_core_info); +public: + explicit BlockIdentifierController(OATPP_COMPONENT(std::shared_ptr, objectMapper)) + : oatpp::web::server::api::ApiController(objectMapper) + {} +public: + + ENDPOINT_INFO(getBlock) { + info->summary = "Get a block by its hash or height (ID)"; + info->addTag("Block"); + info->pathParams["identifier"].description = "The hash (hex string) or height (integer) of the block to retrieve."; + info->addResponse>(Status::CODE_200, "application/json"); + info->addResponse(Status::CODE_404, "text/plain"); + info->addResponse(Status::CODE_400, "text/plain"); + } + ENDPOINT("GET", "/block/{identifier}", getBlock, PATH(String, identifier)) { + + currency::block_rpc_extended_info rpc_details; + bool block_found = false; + + // Check if the identifier consists only of digits + if (identifier->find_first_not_of("0123456789") == std::string::npos) { + // It's a numeric ID (height) + try { + uint64_t height = std::stoull(identifier->c_str()); + block_found = m_core_info->getCore().get_blockchain_storage().get_main_block_rpc_details(height, rpc_details); + } catch (const std::exception& e) { + return createResponse(Status::CODE_400, "Invalid block height format"); + } + } else { + // It's a hash + crypto::hash block_hash{}; + if (!epee::string_tools::hex_to_pod(*identifier, block_hash)) { + return createResponse(Status::CODE_400, "Invalid block hash format"); + } + block_found = m_core_info->getCore().get_blockchain_storage().get_main_block_rpc_details(block_hash, rpc_details); + } + + if (!block_found) { + return createResponse(Status::CODE_404, "Block not found"); + } + + // Common logic to populate the DTO + auto blockDetails = BlockDetailsModel::createShared(); + + blockDetails->id = rpc_details.id; + blockDetails->height = rpc_details.height; + blockDetails->timestamp = rpc_details.timestamp; + blockDetails->actual_timestamp = rpc_details.actual_timestamp; + blockDetails->difficulty = rpc_details.difficulty; + blockDetails->prev_id = rpc_details.prev_id; + blockDetails->is_orphan = rpc_details.is_orphan; + blockDetails->base_reward = rpc_details.base_reward; + blockDetails->summary_reward = rpc_details.summary_reward; + blockDetails->total_fee = rpc_details.total_fee; + blockDetails->penalty = rpc_details.penalty; + blockDetails->already_generated_coins = rpc_details.already_generated_coins; + blockDetails->block_cumulative_size = rpc_details.block_cumulative_size; + blockDetails->total_txs_size = rpc_details.total_txs_size; + blockDetails->cumulative_diff_adjusted = rpc_details.cumulative_diff_adjusted; + blockDetails->cumulative_diff_precise = rpc_details.cumulative_diff_precise; + blockDetails->blob = rpc_details.blob; + blockDetails->miner_text_info = rpc_details.miner_text_info; + blockDetails->type = rpc_details.type; + + auto tx_details_list = oatpp::List>::createShared(); + for(const auto& tx_rpc_info : rpc_details.transactions_details) { + auto tx_model = TransactionDetailsModel::createShared(); + tx_model->id = tx_rpc_info.id; + tx_model->fee = tx_rpc_info.fee; + tx_model->amount = tx_rpc_info.amount; + tx_model->blob_size = tx_rpc_info.blob_size; + tx_model->keeper_block = tx_rpc_info.keeper_block; + tx_model->timestamp = tx_rpc_info.timestamp; + tx_details_list->push_back(tx_model); + } + blockDetails->transactions_details = tx_details_list; + + return createDtoResponse(Status::CODE_200, blockDetails); + } + +}; + +#include OATPP_CODEGEN_END(ApiController) + +#endif /* BlockIdentifierController_hpp */ diff --git a/src/api/modal/meta/page.hpp b/src/api/modal/meta/page.hpp new file mode 100644 index 00000000..b69abd12 --- /dev/null +++ b/src/api/modal/meta/page.hpp @@ -0,0 +1,25 @@ + +#ifndef PAGEDTO_HPP +#define PAGEDTO_HPP + +#include "oatpp/core/macro/codegen.hpp" +#include "oatpp/core/Types.hpp" + +#include OATPP_CODEGEN_BEGIN(DTO) + +template +class PageDto : public oatpp::DTO { + + DTO_INIT(PageDto, DTO); + + DTO_FIELD(UInt32, offset); + DTO_FIELD(UInt32, limit); + DTO_FIELD(UInt32, count); + DTO_FIELD(Vector, items); + +}; + + +#include OATPP_CODEGEN_END(DTO) + +#endif //PAGEDTO_HPP \ No newline at end of file