From d2ad8da8c69ee57920e5dcf545cebdfafdbe2879 Mon Sep 17 00:00:00 2001 From: lordbauer Date: Fri, 6 Feb 2026 10:36:37 +0100 Subject: [PATCH 1/4] feat(minibf): add optional base_path configuration Allows nesting all routes under a custom prefix (e.g., /api/v0) for Blockfrost OpenAPI spec compliance. Backward compatible when unset. --- crates/core/src/config.rs | 4 ++++ crates/minibf/README.md | 12 ++++++++++++ crates/minibf/src/lib.rs | 8 +++++++- docs/content/apis/minibf.mdx | 19 +++++++++++-------- src/bin/dolos/init.rs | 1 + 5 files changed, 35 insertions(+), 9 deletions(-) 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/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..4a68dfe5 100644 --- a/crates/minibf/src/lib.rs +++ b/crates/minibf/src/lib.rs @@ -477,7 +477,13 @@ 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 { + Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash()) + } else { + app.layer(NormalizePathLayer::trim_trailing_slash()) + } } impl dolos_core::Driver for Driver 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 { From b54359632fda0555608ae25687b3c9a7ecc93738 Mon Sep 17 00:00:00 2001 From: "Adrian H." <51023444+adrian1-dot@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:25:44 +0100 Subject: [PATCH 2/4] Update crates/minibf/src/lib.rs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- crates/minibf/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/minibf/src/lib.rs b/crates/minibf/src/lib.rs index 4a68dfe5..6e8344e1 100644 --- a/crates/minibf/src/lib.rs +++ b/crates/minibf/src/lib.rs @@ -480,6 +480,9 @@ where // Optionally nest all routes under base_path if let Some(base_path) = &cfg.base_path { + if base_path.is_empty() || base_path == "/" { + panic!("base_path must not be empty or \"/\""); + } Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash()) } else { app.layer(NormalizePathLayer::trim_trailing_slash()) From 11262973383ab22467b37f8eef708c6721b58b85 Mon Sep 17 00:00:00 2001 From: lordbauer Date: Sun, 15 Feb 2026 22:59:06 +0100 Subject: [PATCH 3/4] fix: add comprehensive base_path validation - Check for leading '/' to prevent Axum panic - Reject wildcard characters - Improve error message with actual value --- crates/minibf/src/lib.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/minibf/src/lib.rs b/crates/minibf/src/lib.rs index 6e8344e1..b199347c 100644 --- a/crates/minibf/src/lib.rs +++ b/crates/minibf/src/lib.rs @@ -478,15 +478,21 @@ where CorsLayer::new() }); - // Optionally nest all routes under base_path - if let Some(base_path) = &cfg.base_path { - if base_path.is_empty() || base_path == "/" { - panic!("base_path must not be empty or \"/\""); - } - Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash()) - } else { - 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('*') { + panic!( + "base_path must start with '/', must not be just '/', and must not contain wildcards; got: \"{}\"", + base_path + ); + } + // Only reach here if valid + Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash()) + } else { + // No base_path configured + app.layer(NormalizePathLayer::trim_trailing_slash()) +} } impl dolos_core::Driver for Driver From f1a5f180eed937f45cc2a9f6d6b1d7762c3abca9 Mon Sep 17 00:00:00 2001 From: lordbauer Date: Fri, 6 Mar 2026 10:44:55 +0100 Subject: [PATCH 4/4] fix(minibf): replace panic with ServeError::ConfigError for invalid base_path --- crates/core/src/lib.rs | 3 +++ crates/minibf/src/lib.rs | 14 +++++++------- src/bin/dolos/minibf.rs | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) 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/src/lib.rs b/crates/minibf/src/lib.rs index b199347c..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, @@ -482,17 +482,17 @@ where 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('*') { - panic!( + 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 - Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash()) + Ok(Router::new().nest(base_path, app).layer(NormalizePathLayer::trim_trailing_slash())) } else { // No base_path configured - app.layer(NormalizePathLayer::trim_trailing_slash()) -} + Ok(app.layer(NormalizePathLayer::trim_trailing_slash())) + } } impl dolos_core::Driver for Driver @@ -507,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/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")