nucleus_substrate_sdk/
client.rs1use anyhow::{Context, Result, bail};
5use nucleus_substrate_core::Receipt;
6use serde_json::Value;
7use std::time::Duration;
8
9pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
10
11#[derive(Debug, Clone)]
12pub struct Client {
13 base_url: String,
14 http: reqwest::Client,
15}
16
17impl Client {
18 pub fn new(base_url: impl Into<String>) -> Result<Self> {
19 let base_url = base_url.into().trim_end_matches('/').to_string();
20 let http = reqwest::Client::builder()
21 .timeout(DEFAULT_TIMEOUT)
22 .build()
23 .context("building reqwest client")?;
24 Ok(Self { base_url, http })
25 }
26
27 fn url(&self, path: &str) -> String {
28 format!("{}{path}", self.base_url)
29 }
30
31 pub async fn agent_card(&self) -> Result<Value> {
32 let url = self.url("/.well-known/agent-card.json");
33 let resp = self
34 .http
35 .get(&url)
36 .send()
37 .await
38 .with_context(|| format!("GET {url}"))?;
39 let status = resp.status();
40 if !status.is_success() {
41 bail!("GET {url} → {status}");
42 }
43 resp.json().await.with_context(|| format!("decoding AgentCard"))
44 }
45
46 pub async fn jwks(&self) -> Result<Value> {
47 let url = self.url("/.well-known/jwks.json");
48 let resp = self
49 .http
50 .get(&url)
51 .send()
52 .await
53 .with_context(|| format!("GET {url}"))?;
54 let status = resp.status();
55 if !status.is_success() {
56 bail!("GET {url} → {status}");
57 }
58 resp.json().await.with_context(|| format!("decoding JWKS"))
59 }
60
61 pub async fn fetch_receipt(&self, auction_id: &str) -> Result<Receipt> {
62 let url = self.url(&format!("/v1/receipts/{auction_id}"));
63 let resp = self
64 .http
65 .get(&url)
66 .send()
67 .await
68 .with_context(|| format!("GET {url}"))?;
69 let status = resp.status();
70 if !status.is_success() {
71 let text = resp.text().await.unwrap_or_default();
72 bail!("GET {url} → {status}: {text}");
73 }
74 let body: Value = resp.json().await.context("decoding receipt body")?;
79 if body.get("session").is_some() && body.get("projections").is_some() {
80 serde_json::from_value(body).context("decoding substrate Receipt")
81 } else {
82 bail!("hub returned a non-substrate receipt shape; upgrade the hub or use the legacy SDK")
83 }
84 }
85
86 pub async fn counters(&self) -> Result<Value> {
87 let url = self.url("/v1/metrics/counters");
88 let resp = self
89 .http
90 .get(&url)
91 .send()
92 .await
93 .with_context(|| format!("GET {url}"))?;
94 let status = resp.status();
95 if !status.is_success() {
96 bail!("GET {url} → {status}");
97 }
98 resp.json().await.with_context(|| format!("decoding counters"))
99 }
100}
101
102#[derive(Debug, thiserror::Error)]
104pub enum HubError {
105 #[error("HTTP transport: {0}")]
106 Transport(String),
107 #[error("hub returned non-success status: {0}")]
108 Status(String),
109 #[error("response decode: {0}")]
110 Decode(String),
111}