bankr_agent_api/
client.rs1use std::time::Duration;
4
5use hpx_transport::{
6 ExchangeClient, TypedResponse,
7 auth::ApiKeyAuth,
8 exchange::{RestClient, RestConfig},
9};
10use tracing::{debug, info, warn};
11
12use crate::{
13 error::BankrError,
14 types::{
15 CancelJobResponse, JobResponse, JobStatus, PromptRequest, PromptResponse, SignRequest,
16 SignResponse, SubmitRequest, SubmitResponse, UserInfoResponse,
17 },
18};
19
20const DEFAULT_BASE_URL: &str = "https://api.bankr.bot";
22
23const DEFAULT_TIMEOUT: Duration = Duration::from_mins(1);
25
26const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(2);
28
29const DEFAULT_MAX_POLL_ATTEMPTS: u32 = 60;
31
32#[derive(Debug)]
37pub struct BankrAgentClient {
38 rest: RestClient<ApiKeyAuth>,
39}
40
41impl BankrAgentClient {
42 pub fn new(api_key: &str) -> Result<Self, BankrError> {
49 Self::with_base_url(api_key, DEFAULT_BASE_URL)
50 }
51
52 pub fn with_base_url(api_key: &str, base_url: &str) -> Result<Self, BankrError> {
59 let config =
60 RestConfig::new(base_url).timeout(DEFAULT_TIMEOUT).user_agent("bankr-sdk-rs/0.1.0");
61
62 let auth = ApiKeyAuth::header("X-API-Key", api_key);
63
64 let rest = RestClient::new(config, auth).map_err(|e| BankrError::Config(e.to_string()))?;
65
66 Ok(Self { rest })
67 }
68
69 pub async fn get_me(&self) -> Result<UserInfoResponse, BankrError> {
77 debug!("GET /agent/me");
78 let resp: TypedResponse<UserInfoResponse> =
79 self.rest.get("/agent/me").await.map_err(transport_err)?;
80 Ok(resp.data)
81 }
82
83 pub async fn submit_prompt(&self, req: &PromptRequest) -> Result<PromptResponse, BankrError> {
91 debug!(prompt = %req.prompt, "POST /agent/prompt");
92 let resp: TypedResponse<PromptResponse> =
93 self.rest.post("/agent/prompt", req).await.map_err(transport_err)?;
94 Ok(resp.data)
95 }
96
97 pub async fn get_job(&self, job_id: &str) -> Result<JobResponse, BankrError> {
105 debug!(job_id, "GET /agent/job/{job_id}");
106 let path = format!("/agent/job/{job_id}");
107 let resp: TypedResponse<JobResponse> = self.rest.get(&path).await.map_err(transport_err)?;
108 Ok(resp.data)
109 }
110
111 pub async fn cancel_job(&self, job_id: &str) -> Result<CancelJobResponse, BankrError> {
115 debug!(job_id, "POST /agent/job/{job_id}/cancel");
116 let path = format!("/agent/job/{job_id}/cancel");
117 let empty = serde_json::json!({});
119 let resp: TypedResponse<CancelJobResponse> =
120 self.rest.post(&path, &empty).await.map_err(transport_err)?;
121 Ok(resp.data)
122 }
123
124 pub async fn sign(&self, req: &SignRequest) -> Result<SignResponse, BankrError> {
132 debug!(sig_type = %req.signature_type, "POST /agent/sign");
133 let resp: TypedResponse<SignResponse> =
134 self.rest.post("/agent/sign", req).await.map_err(transport_err)?;
135 Ok(resp.data)
136 }
137
138 pub async fn submit_transaction(
146 &self,
147 req: &SubmitRequest,
148 ) -> Result<SubmitResponse, BankrError> {
149 debug!(chain_id = req.transaction.chain_id, "POST /agent/submit");
150 let resp: TypedResponse<SubmitResponse> =
151 self.rest.post("/agent/submit", req).await.map_err(transport_err)?;
152 Ok(resp.data)
153 }
154
155 pub async fn prompt_and_wait(&self, req: &PromptRequest) -> Result<JobResponse, BankrError> {
164 self.prompt_and_wait_with(req, DEFAULT_POLL_INTERVAL, DEFAULT_MAX_POLL_ATTEMPTS).await
165 }
166
167 pub async fn prompt_and_wait_with(
169 &self,
170 req: &PromptRequest,
171 interval: Duration,
172 max_attempts: u32,
173 ) -> Result<JobResponse, BankrError> {
174 let prompt_resp = self.submit_prompt(req).await?;
175 info!(job_id = %prompt_resp.job_id, "Job submitted, polling…");
176 self.poll_job(&prompt_resp.job_id, interval, max_attempts).await
177 }
178
179 pub async fn poll_job(
181 &self,
182 job_id: &str,
183 interval: Duration,
184 max_attempts: u32,
185 ) -> Result<JobResponse, BankrError> {
186 for attempt in 1..=max_attempts {
187 let job = self.get_job(job_id).await?;
188 debug!(attempt, status = %job.status, "Poll attempt");
189
190 match job.status {
191 JobStatus::Completed => return Ok(job),
192 JobStatus::Failed => {
193 return Err(BankrError::JobFailed {
194 message: job.error.unwrap_or_else(|| "unknown error".to_owned()),
195 });
196 }
197 JobStatus::Cancelled => return Err(BankrError::JobCancelled),
198 JobStatus::Pending | JobStatus::Processing => {
199 if attempt < max_attempts {
200 tokio::time::sleep(interval).await;
201 }
202 }
203 }
204 }
205
206 warn!(job_id, "Poll timeout reached");
207 Err(BankrError::PollTimeout { attempts: max_attempts })
208 }
209}
210
211fn transport_err(err: impl std::fmt::Display) -> BankrError {
213 BankrError::Transport(err.to_string())
214}