diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 61923e0a..25524fcb 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -627,6 +627,10 @@ pub struct MinibfConfig { pub token_registry_url: Option, pub url: Option, pub max_scan_items: Option, + /// Optional base path for all Blockfrost API endpoints (e.g., "/api/v0"). + /// When set, all API routes will be nested under this path. + /// Set to "/api/v0" for full Blockfrost OpenAPI specification compliance. + pub base_path: Option, } #[derive(Deserialize, Serialize, Clone)] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index da124971..68dc7096 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -308,6 +308,9 @@ impl WalError { #[derive(Debug, Error)] pub enum ServeError { + #[error("invalid configuration: {0}")] + ConfigError(String), + #[error("failed to bind listener")] BindError(std::io::Error), diff --git a/crates/minibf/README.md b/crates/minibf/README.md index 21b32d2b..d24b254c 100644 --- a/crates/minibf/README.md +++ b/crates/minibf/README.md @@ -6,6 +6,18 @@ Blockfrost-compatible HTTP API service for the Dolos Cardano data node. `dolos-minibf` provides a REST API that mimics the Blockfrost API, allowing existing Cardano ecosystem tools to work seamlessly with Dolos without requiring code changes. It serves as an API compatibility layer for existing Blockfrost clients. +### Blockfrost Spec Compliance + +The official [Blockfrost OpenAPI specification](https://github.com/blockfrost/openapi) requires all endpoints to be served under the `/api/v0` base path. Dolos supports this via the `base_path` configuration option: + +```toml +[serve.minibf] +listen_address = "[::]:3000" +base_path = "/api/v0" # For full Blockfrost spec compliance +``` + +When `base_path` is set, **all routes** (including health and metrics) are served under that prefix (e.g., `/api/v0/blocks/latest`, `/api/v0/health`, `/api/v0/metrics`). If omitted, endpoints are served at the root for backward compatibility. + ## Features ### API Compatibility diff --git a/crates/minibf/src/lib.rs b/crates/minibf/src/lib.rs index a7beef57..f2f43084 100644 --- a/crates/minibf/src/lib.rs +++ b/crates/minibf/src/lib.rs @@ -269,7 +269,7 @@ impl Facade { pub struct Driver; -pub fn build_router(cfg: MinibfConfig, domain: D) -> Router +pub fn build_router(cfg: MinibfConfig, domain: D) -> Result where D: Domain + SubmitExt + Clone + Send + Sync + 'static, Option: From, @@ -477,7 +477,22 @@ where } else { CorsLayer::new() }); - app.layer(NormalizePathLayer::trim_trailing_slash()) + + // Optionally nest all routes under base_path + if let Some(base_path) = &cfg.base_path { + // Validate before using + if base_path.is_empty() || base_path == "/" || !base_path.starts_with('/') || base_path.contains('*') { + return Err(ServeError::ConfigError(format!( + "base_path must start with '/', must not be just '/', and must not contain wildcards; got: \"{}\"", + base_path + ))); + } + // Only reach here if valid + Ok(Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash())) + } else { + // No base_path configured + Ok(app.layer(NormalizePathLayer::trim_trailing_slash())) + } } impl dolos_core::Driver for Driver @@ -492,7 +507,7 @@ where type Config = MinibfConfig; async fn run(cfg: Self::Config, domain: D, cancel: C) -> Result<(), ServeError> { - let app = build_router(cfg.clone(), domain); + let app = build_router(cfg.clone(), domain)?; let listener = tokio::net::TcpListener::bind(cfg.listen_address) .await diff --git a/docs/content/apis/minibf.mdx b/docs/content/apis/minibf.mdx index 78324fd9..921497da 100644 --- a/docs/content/apis/minibf.mdx +++ b/docs/content/apis/minibf.mdx @@ -129,19 +129,21 @@ The endpoints required for most tx builders to work are supported. Libraries lik The `serve.minibf` section controls the options for the MiniBF endpoint that can be used by clients. -| property | type | example | -| ------------------ | ------- | -------------------------------- | -| listen_address | string | "[::]:3000" | -| permissive_cors | boolean | true | -| token_registry_url | string | "https://token-registry.io" | -| url | string | "https://minibf.local" | -| max_scan_items | integer | 3000 | +| property | type | example | description | +| ------------------ | ------- | -------------------------------- | ----------------------------------------------------------------- | +| listen_address | string | "[::]:3000" | Local address to listen for incoming connections | +| permissive_cors | boolean | true | Allow cross-origin requests from any origin | +| token_registry_url | string | "https://token-registry.io" | Optional token registry base URL for off-chain asset metadata | +| url | string | "https://minibf.local" | Optional public URL used in the `/` root response | +| max_scan_items | integer | 3000 | Caps page-based scans for heavy endpoints (defaults to 3000) | +| base_path | string | "/api/v0" | Optional base path for API endpoints (Blockfrost spec compliance) | - `listen_address`: the local address (`IP:PORT`) to listen for incoming connections (`[::]` represents any IP address). - `permissive_cors`: allow cross-origin requests from any origin. -- `token_registry_url`: optional token registry base URL used for off-chain asset metadata. +- `token_registry_url`: optional token registry base URL used for off-chain asset metadata. - `url`: optional public URL used in the `/` root response. - `max_scan_items`: caps page-based scans for heavy endpoints (defaults to 3000 if unset). +- `base_path`: optional base path prefix for all endpoints. Set to `"/api/v0"` for full Blockfrost OpenAPI specification compliance. When configured, **all routes** (including health and metrics) will be available under this prefix (e.g., `/api/v0/blocks/latest`, `/api/v0/health`, `/api/v0/metrics`). If omitted, endpoints are served at the root (default behavior for backward compatibility). This is an example of the `serve.minibf` fragment with a `dolos.toml` configuration file. @@ -152,6 +154,7 @@ permissive_cors = true token_registry_url = "https://token-registry.io" url = "https://minibf.local" max_scan_items = 3000 +# base_path = "/api/v0" # Uncomment for Blockfrost OpenAPI spec compliance ``` Some endpoints require additional data tracked under the `chain.track` configuration. Tracking defaults to `true` for all fields, but you can disable specific datasets to reduce storage and CPU costs. Disabling a dataset can cause related endpoints to return `404` or empty results. diff --git a/src/bin/dolos/init.rs b/src/bin/dolos/init.rs index dac5d8ee..eda31cdb 100644 --- a/src/bin/dolos/init.rs +++ b/src/bin/dolos/init.rs @@ -330,6 +330,7 @@ impl ConfigEditor { token_registry_url: None, url: None, max_scan_items: None, + base_path: None, } .into(); } else { diff --git a/src/bin/dolos/minibf.rs b/src/bin/dolos/minibf.rs index 62f32ebf..4c0278bd 100644 --- a/src/bin/dolos/minibf.rs +++ b/src/bin/dolos/minibf.rs @@ -34,7 +34,9 @@ pub async fn run(config: &RootConfig, args: &Args) -> miette::Result<()> { .into_diagnostic() .context("invalid minibf path")?; - let app = dolos_minibf::build_router(minibf.clone(), domain); + let app = dolos_minibf::build_router(minibf.clone(), domain) + .into_diagnostic() + .context("building minibf router")?; let request = Request::builder() .method("GET")