use anyhow::Result;
pub struct RelayResponse {
pub status: u16,
pub body: String,
}
#[derive(Clone)]
pub struct RelayClient {
base: String,
http: reqwest::Client,
#[cfg(feature = "pay")]
payer: Option<std::sync::Arc<solana_sdk::signature::Keypair>>,
}
#[cfg(feature = "pay")]
const USDC_DECIMALS: u8 = 6;
impl RelayClient {
pub fn new(base_url: impl Into<String>) -> Self {
let base = base_url.into();
let base = base.trim_end_matches('/').to_string();
Self {
base,
http: reqwest::Client::new(),
#[cfg(feature = "pay")]
payer: None,
}
}
#[cfg(feature = "pay")]
pub fn with_payer(mut self, payer: std::sync::Arc<solana_sdk::signature::Keypair>) -> Self {
self.payer = Some(payer);
self
}
pub fn base_url(&self) -> &str {
&self.base
}
async fn finish(resp: reqwest::Response) -> Result<RelayResponse> {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
Ok(RelayResponse { status, body })
}
async fn send(&self, req: reqwest::RequestBuilder) -> Result<RelayResponse> {
#[cfg(feature = "pay")]
if self.payer.is_some() {
if let Some(retry) = req.try_clone() {
let first = req.send().await?;
if first.status().as_u16() == 402 {
let body = first.text().await.unwrap_or_default();
if let Some(header) = self.payment_header(&body) {
let paid = retry.header("X-Payment", header).send().await?;
return Self::finish(paid).await;
}
return Ok(RelayResponse { status: 402, body });
}
return Self::finish(first).await;
}
}
Self::finish(req.send().await?).await
}
#[cfg(feature = "pay")]
fn payment_header(&self, body_402: &str) -> Option<String> {
let required: scematica_protocol::PaymentRequired = serde_json::from_str(body_402).ok()?;
let requirements = required.accepts.first()?;
let payer = self.payer.as_ref()?;
let payload =
scematica_protocol::client::build_payment_payload(payer, requirements, USDC_DECIMALS)
.ok()?;
scematica_protocol::client::encode_payment_header(&payload).ok()
}
pub async fn get_signal(&self, kind: &str, mint: &str) -> Result<RelayResponse> {
let url = format!("{}/signal/{}/{}", self.base, kind, mint);
self.send(self.http.get(url)).await
}
pub async fn inference_quote(&self, intent_digest: &str) -> Result<RelayResponse> {
let url = format!("{}/inference/quote", self.base);
let body = serde_json::json!({ "intent_digest": intent_digest });
self.send(self.http.post(url).json(&body)).await
}
pub async fn experience_buy(&self, max_price: u64) -> Result<RelayResponse> {
let url = format!("{}/experience/buy", self.base);
let body = serde_json::json!({ "max_price": max_price });
self.send(self.http.post(url).json(&body)).await
}
pub async fn health(&self) -> Result<RelayResponse> {
let url = format!("{}/health", self.base);
self.send(self.http.get(url)).await
}
}