use anyhow::{Context, Result, bail};
use nucleus_substrate_core::Receipt;
use serde_json::Value;
use std::time::Duration;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Debug, Clone)]
pub struct Client {
base_url: String,
http: reqwest::Client,
}
impl Client {
pub fn new(base_url: impl Into<String>) -> Result<Self> {
let base_url = base_url.into().trim_end_matches('/').to_string();
let http = reqwest::Client::builder()
.timeout(DEFAULT_TIMEOUT)
.build()
.context("building reqwest client")?;
Ok(Self { base_url, http })
}
fn url(&self, path: &str) -> String {
format!("{}{path}", self.base_url)
}
pub async fn agent_card(&self) -> Result<Value> {
let url = self.url("/.well-known/agent-card.json");
let resp = self
.http
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
let status = resp.status();
if !status.is_success() {
bail!("GET {url} → {status}");
}
resp.json().await.with_context(|| format!("decoding AgentCard"))
}
pub async fn jwks(&self) -> Result<Value> {
let url = self.url("/.well-known/jwks.json");
let resp = self
.http
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
let status = resp.status();
if !status.is_success() {
bail!("GET {url} → {status}");
}
resp.json().await.with_context(|| format!("decoding JWKS"))
}
pub async fn fetch_receipt(&self, auction_id: &str) -> Result<Receipt> {
let url = self.url(&format!("/v1/receipts/{auction_id}"));
let resp = self
.http
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
bail!("GET {url} → {status}: {text}");
}
let body: Value = resp.json().await.context("decoding receipt body")?;
if body.get("session").is_some() && body.get("projections").is_some() {
serde_json::from_value(body).context("decoding substrate Receipt")
} else {
bail!("hub returned a non-substrate receipt shape; upgrade the hub or use the legacy SDK")
}
}
pub async fn counters(&self) -> Result<Value> {
let url = self.url("/v1/metrics/counters");
let resp = self
.http
.get(&url)
.send()
.await
.with_context(|| format!("GET {url}"))?;
let status = resp.status();
if !status.is_success() {
bail!("GET {url} → {status}");
}
resp.json().await.with_context(|| format!("decoding counters"))
}
}
#[derive(Debug, thiserror::Error)]
pub enum HubError {
#[error("HTTP transport: {0}")]
Transport(String),
#[error("hub returned non-success status: {0}")]
Status(String),
#[error("response decode: {0}")]
Decode(String),
}