Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions backend/crates/atlas-server/src/api/handlers/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ use std::sync::Arc;

use crate::api::AppState;

#[derive(Serialize)]
pub struct ChainFeatures {
pub da_tracking: bool,
}

#[derive(Serialize)]
pub struct FaucetConfig {
pub enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub amount_wei: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cooldown_minutes: Option<u64>,
}

#[derive(Serialize)]
pub struct BrandingConfig {
pub chain_name: String,
Expand All @@ -23,6 +37,8 @@ pub struct BrandingConfig {
pub success_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_color: Option<String>,
pub features: ChainFeatures,
pub faucet: FaucetConfig,
}

/// GET /api/config - Returns white-label branding configuration
Expand All @@ -38,6 +54,14 @@ pub async fn get_config(State(state): State<Arc<AppState>>) -> Json<BrandingConf
background_color_light: state.background_color_light.clone(),
success_color: state.success_color.clone(),
error_color: state.error_color.clone(),
features: ChainFeatures {
da_tracking: state.da_tracking_enabled,
},
faucet: FaucetConfig {
enabled: state.faucet.is_some(),
amount_wei: state.faucet_amount_wei.clone(),
cooldown_minutes: state.faucet_cooldown_minutes,
},
})
}

Expand All @@ -57,17 +81,27 @@ mod tests {
background_color_light: None,
success_color: None,
error_color: None,
features: ChainFeatures { da_tracking: false },
faucet: FaucetConfig {
enabled: false,
amount_wei: None,
cooldown_minutes: None,
},
};

let json = serde_json::to_value(&config).unwrap();
assert_eq!(json["chain_name"], "TestChain");
assert_eq!(json["accent_color"], "#3b82f6");
assert_eq!(json["features"]["da_tracking"], false);
assert_eq!(json["faucet"]["enabled"], false);
assert!(json.get("logo_url").is_none());
assert!(json.get("logo_url_light").is_none());
assert!(json.get("logo_url_dark").is_none());
assert!(json.get("background_color_dark").is_none());
assert!(json.get("success_color").is_none());
assert!(json.get("error_color").is_none());
assert!(json["faucet"].get("amount_wei").is_none());
assert!(json["faucet"].get("cooldown_minutes").is_none());
}

#[test]
Expand All @@ -82,6 +116,12 @@ mod tests {
background_color_light: Some("#faf5ef".to_string()),
success_color: Some("#10b981".to_string()),
error_color: Some("#ef4444".to_string()),
features: ChainFeatures { da_tracking: true },
faucet: FaucetConfig {
enabled: true,
amount_wei: Some("100000000000000000".to_string()),
cooldown_minutes: Some(30),
},
};

let json = serde_json::to_value(&config).unwrap();
Expand All @@ -94,5 +134,9 @@ mod tests {
assert_eq!(json["background_color_light"], "#faf5ef");
assert_eq!(json["success_color"], "#10b981");
assert_eq!(json["error_color"], "#ef4444");
assert_eq!(json["features"]["da_tracking"], true);
assert_eq!(json["faucet"]["enabled"], true);
assert_eq!(json["faucet"]["amount_wei"], "100000000000000000");
assert_eq!(json["faucet"]["cooldown_minutes"], 30);
}
}
2 changes: 2 additions & 0 deletions backend/crates/atlas-server/src/api/handlers/faucet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ mod tests {
rpc_url: String::new(),
da_tracking_enabled: false,
faucet,
faucet_amount_wei: None,
faucet_cooldown_minutes: None,
chain_id: 1,
chain_name: "Test Chain".to_string(),
chain_logo_url: None,
Expand Down
2 changes: 2 additions & 0 deletions backend/crates/atlas-server/src/api/handlers/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ mod tests {
rpc_url: String::new(),
da_tracking_enabled: false,
faucet: None,
faucet_amount_wei: None,
faucet_cooldown_minutes: None,
chain_id: 1,
chain_name: "Test Chain".to_string(),
chain_logo_url: None,
Expand Down
2 changes: 2 additions & 0 deletions backend/crates/atlas-server/src/api/handlers/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ mod tests {
rpc_url: String::new(),
da_tracking_enabled: false,
faucet: None,
faucet_amount_wei: None,
faucet_cooldown_minutes: None,
chain_id: 1,
chain_name: "Test Chain".to_string(),
chain_logo_url: None,
Expand Down
13 changes: 2 additions & 11 deletions backend/crates/atlas-server/src/api/handlers/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,10 @@ use crate::api::error::ApiResult;
use crate::api::handlers::get_table_count;
use crate::api::AppState;

#[derive(Serialize)]
pub struct ChainFeatures {
pub da_tracking: bool,
}

#[derive(Serialize)]
pub struct HeightResponse {
pub block_height: i64,
pub indexed_at: String,
pub features: ChainFeatures,
}

#[derive(Serialize)]
Expand Down Expand Up @@ -57,14 +51,10 @@ async fn latest_height_and_indexed_at(state: &AppState) -> Result<(i64, String),
/// Returns in <1ms, optimized for frequent polling.
pub async fn get_height(State(state): State<Arc<AppState>>) -> ApiResult<Json<HeightResponse>> {
let (block_height, indexed_at) = latest_height_and_indexed_at(&state).await?;
let features = ChainFeatures {
da_tracking: state.da_tracking_enabled,
};

Ok(Json(HeightResponse {
block_height,
indexed_at,
features,
}))
}

Expand Down Expand Up @@ -122,6 +112,8 @@ mod tests {
rpc_url: String::new(),
da_tracking_enabled: false,
faucet: None,
faucet_amount_wei: None,
faucet_cooldown_minutes: None,
chain_id: 1,
chain_name: "Test Chain".to_string(),
chain_logo_url: None,
Expand Down Expand Up @@ -149,7 +141,6 @@ mod tests {

assert_eq!(status.block_height, 42);
assert!(!status.indexed_at.is_empty());
assert!(!status.features.da_tracking);
}

#[tokio::test]
Expand Down
4 changes: 4 additions & 0 deletions backend/crates/atlas-server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub struct AppState {
pub rpc_url: String,
pub da_tracking_enabled: bool,
pub faucet: Option<SharedFaucetBackend>,
pub faucet_amount_wei: Option<String>,
pub faucet_cooldown_minutes: Option<u64>,
pub chain_id: u64,
pub chain_name: String,
pub chain_logo_url: Option<String>,
Expand Down Expand Up @@ -285,6 +287,8 @@ mod tests {
rpc_url: String::new(),
da_tracking_enabled: false,
faucet,
faucet_amount_wei: None,
faucet_cooldown_minutes: None,
chain_id: 1,
chain_name: "Test Chain".to_string(),
chain_logo_url: None,
Expand Down
4 changes: 4 additions & 0 deletions backend/crates/atlas-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ async fn run(args: cli::RunArgs) -> Result<()> {
let config = config::Config::from_run_args(args.clone())?;
let faucet_config = config::FaucetConfig::from_faucet_args(&args.faucet)?;
let snapshot_config = config::SnapshotConfig::from_env(&config.database_url)?;
let faucet_amount_wei = faucet_config.amount_wei.as_ref().map(ToString::to_string);
let faucet_cooldown_minutes = faucet_config.cooldown_minutes;

let faucet = if faucet_config.enabled {
tracing::info!("Faucet enabled");
Expand Down Expand Up @@ -358,6 +360,8 @@ async fn run(args: cli::RunArgs) -> Result<()> {
rpc_url: config.rpc_url.clone(),
da_tracking_enabled: config.da_tracking_enabled,
faucet,
faucet_amount_wei,
faucet_cooldown_minutes,
chain_id,
chain_name: config.chain_name.clone(),
chain_logo_url: config.chain_logo_url.clone(),
Expand Down
2 changes: 2 additions & 0 deletions backend/crates/atlas-server/tests/integration/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub fn test_router() -> Router {
rpc_url: String::new(),
da_tracking_enabled: false,
faucet: None,
faucet_amount_wei: None,
faucet_cooldown_minutes: None,
chain_id: 42,
chain_name: "Test Chain".to_string(),
chain_logo_url: None,
Expand Down
59 changes: 57 additions & 2 deletions frontend/src/api/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import client from './client';
import client from "./client";
import type { ChainFeatures } from "../types";

export interface FaucetConfig {
enabled: boolean;
amount_wei?: string;
cooldown_minutes?: number;
}

export interface BrandingConfig {
chain_name: string;
Expand All @@ -10,8 +17,56 @@ export interface BrandingConfig {
background_color_light?: string;
success_color?: string;
error_color?: string;
features: ChainFeatures;
faucet: FaucetConfig;
}

const defaultFeatures: ChainFeatures = { da_tracking: false };
const defaultFaucet: FaucetConfig = { enabled: false };

function normalizeFeatures(value: unknown): ChainFeatures {
if (!value || typeof value !== "object") {
return defaultFeatures;
}

const features = value as Partial<ChainFeatures>;
return {
da_tracking: features.da_tracking === true,
};
}

function normalizeFaucet(value: unknown): FaucetConfig {
if (!value || typeof value !== "object") {
return defaultFaucet;
}

const faucet = value as Partial<FaucetConfig>;
return {
enabled: faucet.enabled === true,
...(typeof faucet.amount_wei === "string"
? { amount_wei: faucet.amount_wei }
: {}),
...(Number.isFinite(faucet.cooldown_minutes) &&
Number.isInteger(faucet.cooldown_minutes) &&
faucet.cooldown_minutes >= 0
? { cooldown_minutes: faucet.cooldown_minutes }
: {}),
};
}

export function normalizeBrandingConfig(
config: Partial<BrandingConfig> & Pick<BrandingConfig, "chain_name">,
): BrandingConfig {
return {
...config,
features: normalizeFeatures(config.features),
faucet: normalizeFaucet(config.faucet),
};
}

export async function getConfig(): Promise<BrandingConfig> {
return client.get<BrandingConfig>('/config');
const config = await client.get<
Partial<BrandingConfig> & Pick<BrandingConfig, "chain_name">
>("/config");
return normalizeBrandingConfig(config);
}
8 changes: 3 additions & 5 deletions frontend/src/api/status.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import client from './client';
import type { ChainFeatures } from '../types';
import client from "./client";

export interface HeightResponse {
block_height: number;
indexed_at?: string; // ISO timestamp, absent when no blocks indexed
features: ChainFeatures;
}

export interface ChainStatusResponse {
Expand All @@ -17,9 +15,9 @@ export interface ChainStatusResponse {
}

export async function getHeight(): Promise<HeightResponse> {
return client.get<HeightResponse>('/height');
return client.get<HeightResponse>("/height");
}

export async function getChainStatus(): Promise<ChainStatusResponse> {
return client.get<ChainStatusResponse>('/status');
return client.get<ChainStatusResponse>("/status");
}
Loading
Loading