diff --git a/crates/aldabra-chain/src/koios.rs b/crates/aldabra-chain/src/koios.rs index 80d00f7..eb36195 100644 --- a/crates/aldabra-chain/src/koios.rs +++ b/crates/aldabra-chain/src/koios.rs @@ -108,17 +108,43 @@ pub struct KoiosClient { } impl KoiosClient { - /// Construct a client with the default 10-second timeout. + /// Construct a client with the default 10-second timeout and no + /// bearer (public-tier; subject to free-tier daily quotas). pub fn new(base_url: impl Into) -> Self { - Self::with_timeout(base_url, DEFAULT_TIMEOUT) + Self::with_timeout_and_bearer(base_url, DEFAULT_TIMEOUT, None) } - /// Construct a client with a custom request timeout. + /// Construct a client with a custom request timeout, no bearer. pub fn with_timeout(base_url: impl Into, timeout: Duration) -> Self { + Self::with_timeout_and_bearer(base_url, timeout, None) + } + + /// Construct a client with optional `Authorization: Bearer ` + /// applied to every request. Used for paid-tier Koios access — the + /// JWT comes from the operator-supplied `ALDABRA_KOIOS_BEARER` env + /// var (NEVER from the on-disk config, NEVER hardcoded). Pass + /// `None` for the free public tier. + pub fn with_timeout_and_bearer( + base_url: impl Into, + timeout: Duration, + bearer: Option<&str>, + ) -> Self { + let mut builder = Client::builder().timeout(timeout); + if let Some(token) = bearer { + // Default header is applied to every request the client + // emits — request-level overrides still possible but no + // builder code path needs to remember to set it. + let mut hdrs = reqwest::header::HeaderMap::new(); + let value = format!("Bearer {token}"); + let mut hv = reqwest::header::HeaderValue::from_str(&value) + .expect("ALDABRA_KOIOS_BEARER contains invalid header bytes"); + hv.set_sensitive(true); + hdrs.insert(reqwest::header::AUTHORIZATION, hv); + builder = builder.default_headers(hdrs); + } Self { base_url: base_url.into(), - http: Client::builder() - .timeout(timeout) + http: builder .build() .expect("reqwest client builds with rustls + json features"), } diff --git a/crates/aldabra-dao/src/discovery.rs b/crates/aldabra-dao/src/discovery.rs index 2d3fafe..aa8e4dd 100644 --- a/crates/aldabra-dao/src/discovery.rs +++ b/crates/aldabra-dao/src/discovery.rs @@ -63,12 +63,27 @@ pub struct KoiosDiscoveryClient { impl KoiosDiscoveryClient { pub fn new(base_url: impl Into) -> Self { + Self::with_bearer(base_url, None) + } + + /// Same as [`Self::new`] but with an optional `Authorization: Bearer + /// ` default header for paid-tier Koios access. Bearer comes + /// from `ALDABRA_KOIOS_BEARER` env var only — never from disk. + pub fn with_bearer(base_url: impl Into, bearer: Option<&str>) -> Self { + let mut builder = + reqwest::Client::builder().timeout(std::time::Duration::from_secs(30)); + if let Some(token) = bearer { + let mut hdrs = reqwest::header::HeaderMap::new(); + let value = format!("Bearer {token}"); + let mut hv = reqwest::header::HeaderValue::from_str(&value) + .expect("ALDABRA_KOIOS_BEARER contains invalid header bytes"); + hv.set_sensitive(true); + hdrs.insert(reqwest::header::AUTHORIZATION, hv); + builder = builder.default_headers(hdrs); + } Self { base_url: base_url.into(), - http: reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .expect("reqwest client"), + http: builder.build().expect("reqwest client"), } } } diff --git a/crates/aldabra-dao/src/reader.rs b/crates/aldabra-dao/src/reader.rs index 815d94d..1051ae4 100644 --- a/crates/aldabra-dao/src/reader.rs +++ b/crates/aldabra-dao/src/reader.rs @@ -93,12 +93,27 @@ impl KoiosDaoReader { /// Construct against a Koios base URL (e.g. `https://api.koios.rest/api/v1` /// or `https://preprod.koios.rest/api/v1`). pub fn new(base_url: impl Into) -> Self { + Self::with_bearer(base_url, None) + } + + /// Same as [`Self::new`] but with an optional `Authorization: Bearer + /// ` default header for paid-tier Koios access. Bearer is + /// supplied by the caller from `ALDABRA_KOIOS_BEARER` env var only. + pub fn with_bearer(base_url: impl Into, bearer: Option<&str>) -> Self { + let mut builder = + reqwest::Client::builder().timeout(std::time::Duration::from_secs(30)); + if let Some(token) = bearer { + let mut hdrs = reqwest::header::HeaderMap::new(); + let value = format!("Bearer {token}"); + let mut hv = reqwest::header::HeaderValue::from_str(&value) + .expect("ALDABRA_KOIOS_BEARER contains invalid header bytes"); + hv.set_sensitive(true); + hdrs.insert(reqwest::header::AUTHORIZATION, hv); + builder = builder.default_headers(hdrs); + } Self { base_url: base_url.into(), - http: reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(30)) - .build() - .expect("reqwest client"), + http: builder.build().expect("reqwest client"), } } diff --git a/crates/aldabra-mcp/src/config.rs b/crates/aldabra-mcp/src/config.rs index 2cb494c..ebf695a 100644 --- a/crates/aldabra-mcp/src/config.rs +++ b/crates/aldabra-mcp/src/config.rs @@ -20,6 +20,13 @@ use thiserror::Error; pub struct Config { pub network: Network, pub koios_base: String, + /// Optional Koios bearer token (paid-tier JWT). Sourced from the + /// `ALDABRA_KOIOS_BEARER` env var only — never from the TOML file + /// or CLI args. Bearers are credentials and must not get persisted + /// alongside non-secret config. When set, every Koios request gets + /// `Authorization: Bearer ` and bypasses the public-tier + /// daily quota. + pub koios_bearer: Option, pub account: u32, pub index: u32, pub data_dir: PathBuf, @@ -138,6 +145,13 @@ impl Config { .or(file_cfg.koios_base) .unwrap_or_else(|| default_koios_for(network).to_string()); + // Koios bearer is env-only — never sourced from disk. Empty + // string is treated as "no bearer" so that an unset systemd + // EnvironmentFile entry doesn't accidentally send `Bearer ""`. + let koios_bearer = std::env::var("ALDABRA_KOIOS_BEARER") + .ok() + .filter(|s| !s.trim().is_empty()); + let account = match std::env::var("ALDABRA_ACCOUNT") { Ok(s) => s .parse::() @@ -165,6 +179,7 @@ impl Config { Ok(Self { network, koios_base, + koios_bearer, account, index, data_dir, diff --git a/crates/aldabra-mcp/src/main.rs b/crates/aldabra-mcp/src/main.rs index e6c94d3..f82dcfd 100644 --- a/crates/aldabra-mcp/src/main.rs +++ b/crates/aldabra-mcp/src/main.rs @@ -52,6 +52,7 @@ async fn run() -> Result<()> { tracing::info!( network = ?cfg.network, koios = %cfg.koios_base, + koios_bearer_set = cfg.koios_bearer.is_some(), account = cfg.account, index = cfg.index, data_dir = %cfg.data_dir.display(), @@ -131,6 +132,7 @@ async fn run() -> Result<()> { cfg.network, address, cfg.koios_base, + cfg.koios_bearer, payment_key, stake_key, cfg.max_send_lovelace, diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index 9e87003..cb61fdd 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -165,6 +165,12 @@ struct WalletInner { /// Cached Koios base url so `dao_discover_scripts` can spin up a /// `KoiosDiscoveryClient` on demand without a re-construction call. koios_base: String, + /// Cached Koios bearer token so on-demand `KoiosDiscoveryClient` + /// (and any future per-call client) inherits the same paid-tier + /// auth instead of falling back to free-tier and tripping daily + /// quotas. None = public tier. Sourced from env only — never from + /// disk; never logged. + koios_bearer: Option, } impl WalletService { @@ -172,22 +178,29 @@ impl WalletService { network: Network, address: String, koios_base: String, + koios_bearer: Option, payment_key: PaymentKey, stake_key: StakeKey, max_send_lovelace: u64, data_dir: PathBuf, ) -> Self { + let bearer_ref = koios_bearer.as_deref(); Self { inner: Arc::new(WalletInner { network, address, - chain: KoiosClient::new(koios_base.clone()), + chain: KoiosClient::with_timeout_and_bearer( + koios_base.clone(), + std::time::Duration::from_secs(10), + bearer_ref, + ), payment_key, stake_key, max_send_lovelace, dao_store: DaoStore::new(&data_dir), - dao_reader: KoiosDaoReader::new(koios_base.clone()), + dao_reader: KoiosDaoReader::with_bearer(koios_base.clone(), bearer_ref), koios_base, + koios_bearer, }), } } @@ -2145,8 +2158,11 @@ impl WalletService { let mut deployers: Vec<&str> = vec![MAINNET_AGORA_SHARED_DEPLOYER]; deployers.extend(extra.iter().map(|s| s.as_str())); - // Use the same Koios base URL as the wallet's chain backend. - let client = KoiosDiscoveryClient::new(self.inner.koios_base.clone()); + // Use the same Koios base URL + bearer as the wallet's chain backend. + let client = KoiosDiscoveryClient::with_bearer( + self.inner.koios_base.clone(), + self.inner.koios_bearer.as_deref(), + ); let report = discover_scripts(&cfg, &client, &deployers) .await .map_err(|e| e.to_string())?;