use std::time::Duration;
use hpx_transport::{
ExchangeClient, TypedResponse,
auth::ApiKeyAuth,
exchange::{RestClient, RestConfig},
};
use tracing::{debug, info, warn};
use crate::{
error::BankrError,
types::{
CancelJobResponse, JobResponse, JobStatus, PromptRequest, PromptResponse, SignRequest,
SignResponse, SubmitRequest, SubmitResponse, UserInfoResponse,
},
};
const DEFAULT_BASE_URL: &str = "https://api.bankr.bot";
const DEFAULT_TIMEOUT: Duration = Duration::from_mins(1);
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(2);
const DEFAULT_MAX_POLL_ATTEMPTS: u32 = 60;
#[derive(Debug)]
pub struct BankrAgentClient {
rest: RestClient<ApiKeyAuth>,
}
impl BankrAgentClient {
pub fn new(api_key: &str) -> Result<Self, BankrError> {
Self::with_base_url(api_key, DEFAULT_BASE_URL)
}
pub fn with_base_url(api_key: &str, base_url: &str) -> Result<Self, BankrError> {
let config =
RestConfig::new(base_url).timeout(DEFAULT_TIMEOUT).user_agent("bankr-sdk-rs/0.1.0");
let auth = ApiKeyAuth::header("X-API-Key", api_key);
let rest = RestClient::new(config, auth).map_err(|e| BankrError::Config(e.to_string()))?;
Ok(Self { rest })
}
pub async fn get_me(&self) -> Result<UserInfoResponse, BankrError> {
debug!("GET /agent/me");
let resp: TypedResponse<UserInfoResponse> =
self.rest.get("/agent/me").await.map_err(transport_err)?;
Ok(resp.data)
}
pub async fn submit_prompt(&self, req: &PromptRequest) -> Result<PromptResponse, BankrError> {
debug!(prompt = %req.prompt, "POST /agent/prompt");
let resp: TypedResponse<PromptResponse> =
self.rest.post("/agent/prompt", req).await.map_err(transport_err)?;
Ok(resp.data)
}
pub async fn get_job(&self, job_id: &str) -> Result<JobResponse, BankrError> {
debug!(job_id, "GET /agent/job/{job_id}");
let path = format!("/agent/job/{job_id}");
let resp: TypedResponse<JobResponse> = self.rest.get(&path).await.map_err(transport_err)?;
Ok(resp.data)
}
pub async fn cancel_job(&self, job_id: &str) -> Result<CancelJobResponse, BankrError> {
debug!(job_id, "POST /agent/job/{job_id}/cancel");
let path = format!("/agent/job/{job_id}/cancel");
let empty = serde_json::json!({});
let resp: TypedResponse<CancelJobResponse> =
self.rest.post(&path, &empty).await.map_err(transport_err)?;
Ok(resp.data)
}
pub async fn sign(&self, req: &SignRequest) -> Result<SignResponse, BankrError> {
debug!(sig_type = %req.signature_type, "POST /agent/sign");
let resp: TypedResponse<SignResponse> =
self.rest.post("/agent/sign", req).await.map_err(transport_err)?;
Ok(resp.data)
}
pub async fn submit_transaction(
&self,
req: &SubmitRequest,
) -> Result<SubmitResponse, BankrError> {
debug!(chain_id = req.transaction.chain_id, "POST /agent/submit");
let resp: TypedResponse<SubmitResponse> =
self.rest.post("/agent/submit", req).await.map_err(transport_err)?;
Ok(resp.data)
}
pub async fn prompt_and_wait(&self, req: &PromptRequest) -> Result<JobResponse, BankrError> {
self.prompt_and_wait_with(req, DEFAULT_POLL_INTERVAL, DEFAULT_MAX_POLL_ATTEMPTS).await
}
pub async fn prompt_and_wait_with(
&self,
req: &PromptRequest,
interval: Duration,
max_attempts: u32,
) -> Result<JobResponse, BankrError> {
let prompt_resp = self.submit_prompt(req).await?;
info!(job_id = %prompt_resp.job_id, "Job submitted, polling…");
self.poll_job(&prompt_resp.job_id, interval, max_attempts).await
}
pub async fn poll_job(
&self,
job_id: &str,
interval: Duration,
max_attempts: u32,
) -> Result<JobResponse, BankrError> {
for attempt in 1..=max_attempts {
let job = self.get_job(job_id).await?;
debug!(attempt, status = %job.status, "Poll attempt");
match job.status {
JobStatus::Completed => return Ok(job),
JobStatus::Failed => {
return Err(BankrError::JobFailed {
message: job.error.unwrap_or_else(|| "unknown error".to_owned()),
});
}
JobStatus::Cancelled => return Err(BankrError::JobCancelled),
JobStatus::Pending | JobStatus::Processing => {
if attempt < max_attempts {
tokio::time::sleep(interval).await;
}
}
}
}
warn!(job_id, "Poll timeout reached");
Err(BankrError::PollTimeout { attempts: max_attempts })
}
}
fn transport_err(err: impl std::fmt::Display) -> BankrError {
BankrError::Transport(err.to_string())
}