1use anyhow::{anyhow, Context, Result};
7use reqwest::{Method, StatusCode};
8use serde_json::{json, Value};
9
10pub const PREDICT_MAINNET_BASE: &str = "https://api.predict.fun/v1";
11pub const PREDICT_TESTNET_BASE: &str = "https://api-testnet.predict.fun/v1";
12
13#[derive(Debug, Clone)]
15pub struct RawApiResponse {
16 pub status: StatusCode,
17 pub json: Option<Value>,
18}
19
20#[derive(Clone)]
27pub struct PredictApiClient {
28 base: String,
29 api_key: Option<String>,
30 jwt: Option<String>,
31 http: reqwest::Client,
32}
33
34impl PredictApiClient {
35 pub fn new_mainnet(api_key: impl Into<String>) -> Result<Self> {
36 Self::new(PREDICT_MAINNET_BASE, Some(api_key.into()), None)
37 }
38
39 pub fn new_testnet() -> Result<Self> {
40 Self::new(PREDICT_TESTNET_BASE, None, None)
41 }
42
43 pub fn new(
44 base: impl Into<String>,
45 api_key: Option<String>,
46 jwt: Option<String>,
47 ) -> Result<Self> {
48 let http = reqwest::Client::builder()
49 .timeout(std::time::Duration::from_secs(10))
50 .connect_timeout(std::time::Duration::from_secs(5))
51 .pool_max_idle_per_host(16)
52 .pool_idle_timeout(std::time::Duration::from_secs(90))
53 .tcp_keepalive(std::time::Duration::from_secs(30))
54 .build()
55 .context("failed to build predict api client")?;
56
57 Ok(Self {
58 base: base.into().trim_end_matches('/').to_string(),
59 api_key,
60 jwt,
61 http,
62 })
63 }
64
65 pub fn with_jwt(mut self, jwt: impl Into<String>) -> Self {
66 self.jwt = Some(jwt.into());
67 self
68 }
69
70 pub fn set_jwt(&mut self, jwt: impl Into<String>) {
71 self.jwt = Some(jwt.into());
72 }
73
74 pub fn clear_jwt(&mut self) {
75 self.jwt = None;
76 }
77
78 pub fn has_jwt(&self) -> bool {
79 self.jwt.is_some()
80 }
81
82 pub async fn auth_message(&self) -> Result<Value> {
85 self.get_ok("/auth/message", &[], false).await
86 }
87
88 pub async fn auth(&self, signer: &str, message: &str, signature: &str) -> Result<Value> {
89 let body = json!({
90 "signer": signer,
91 "message": message,
92 "signature": signature,
93 });
94 self.post_ok("/auth", &[], body, false).await
95 }
96
97 pub async fn create_order(&self, body: Value) -> Result<Value> {
100 self.post_ok("/orders", &[], body, true).await
101 }
102
103 pub async fn list_orders(&self, query: &[(&str, String)]) -> Result<Value> {
104 self.get_ok("/orders", query, true).await
105 }
106
107 pub async fn remove_orders(&self, body: Value) -> Result<Value> {
108 self.post_ok("/orders/remove", &[], body, true).await
109 }
110
111 pub async fn get_order(&self, hash: &str) -> Result<Value> {
112 self.get_ok(&format!("/orders/{}", hash), &[], true).await
113 }
114
115 pub async fn get_order_matches(&self, query: &[(&str, String)]) -> Result<Value> {
116 self.get_ok("/orders/matches", query, false).await
117 }
118
119 pub async fn list_markets(&self, query: &[(&str, String)]) -> Result<Value> {
122 self.get_ok("/markets", query, false).await
123 }
124
125 pub async fn get_market(&self, id: i64) -> Result<Value> {
126 self.get_ok(&format!("/markets/{}", id), &[], false).await
127 }
128
129 pub async fn get_market_stats(&self, id: i64) -> Result<Value> {
130 self.get_ok(&format!("/markets/{}/stats", id), &[], false)
131 .await
132 }
133
134 pub async fn get_market_last_sale(&self, id: i64) -> Result<Value> {
135 self.get_ok(&format!("/markets/{}/last-sale", id), &[], false)
136 .await
137 }
138
139 pub async fn get_market_orderbook(&self, id: i64) -> Result<Value> {
140 self.get_ok(&format!("/markets/{}/orderbook", id), &[], false)
141 .await
142 }
143
144 pub async fn get_market_timeseries(&self, id: i64, query: &[(&str, String)]) -> Result<Value> {
145 self.get_ok(&format!("/markets/{}/timeseries", id), query, false)
146 .await
147 }
148
149 pub async fn get_market_timeseries_latest(&self, id: i64) -> Result<Value> {
150 self.get_ok(&format!("/markets/{}/timeseries/latest", id), &[], false)
151 .await
152 }
153
154 pub async fn list_categories(&self, query: &[(&str, String)]) -> Result<Value> {
157 self.get_ok("/categories", query, false).await
158 }
159
160 pub async fn get_category(&self, slug: &str) -> Result<Value> {
161 self.get_ok(&format!("/categories/{}", slug), &[], false)
162 .await
163 }
164
165 pub async fn get_category_stats(&self, id: i64) -> Result<Value> {
166 self.get_ok(&format!("/categories/{}/stats", id), &[], false)
167 .await
168 }
169
170 pub async fn list_tags(&self) -> Result<Value> {
171 self.get_ok("/tags", &[], false).await
172 }
173
174 pub async fn list_positions(&self, query: &[(&str, String)]) -> Result<Value> {
177 self.get_ok("/positions", query, true).await
178 }
179
180 pub async fn list_positions_for_address(
181 &self,
182 address: &str,
183 query: &[(&str, String)],
184 ) -> Result<Value> {
185 self.get_ok(&format!("/positions/{}", address), query, false)
186 .await
187 }
188
189 pub async fn account(&self) -> Result<Value> {
192 self.get_ok("/account", &[], true).await
193 }
194
195 pub async fn set_referral(&self, code: &str) -> Result<Value> {
196 let body = json!({ "code": code });
197 self.post_ok("/account/referral", &[], body, true).await
198 }
199
200 pub async fn account_activity(&self, query: &[(&str, String)]) -> Result<Value> {
201 self.get_ok("/account/activity", query, true).await
202 }
203
204 pub async fn oauth_finalize(&self, body: Value) -> Result<Value> {
207 self.post_ok("/oauth/finalize", &[], body, false).await
208 }
209
210 pub async fn oauth_orders(&self, body: Value) -> Result<Value> {
211 self.post_ok("/oauth/orders", &[], body, false).await
212 }
213
214 pub async fn oauth_create_order(&self, body: Value) -> Result<Value> {
215 self.post_ok("/oauth/orders/create", &[], body, false).await
216 }
217
218 pub async fn oauth_cancel_order(&self, body: Value) -> Result<Value> {
219 self.post_ok("/oauth/orders/cancel", &[], body, false).await
220 }
221
222 pub async fn oauth_positions(&self, body: Value) -> Result<Value> {
223 self.post_ok("/oauth/positions", &[], body, false).await
224 }
225
226 pub async fn search(&self, query: &[(&str, String)]) -> Result<Value> {
229 self.get_ok("/search", query, false).await
230 }
231
232 pub async fn yield_pending(&self, query: &[(&str, String)]) -> Result<Value> {
233 self.get_ok("/yield/pending", query, true).await
234 }
235
236 pub async fn raw_get(
239 &self,
240 path: &str,
241 query: &[(&str, String)],
242 require_jwt: bool,
243 ) -> Result<RawApiResponse> {
244 self.raw_request(Method::GET, path, query, None, require_jwt)
245 .await
246 }
247
248 pub async fn raw_post(
249 &self,
250 path: &str,
251 query: &[(&str, String)],
252 body: Value,
253 require_jwt: bool,
254 ) -> Result<RawApiResponse> {
255 self.raw_request(Method::POST, path, query, Some(body), require_jwt)
256 .await
257 }
258
259 async fn get_ok(
262 &self,
263 path: &str,
264 query: &[(&str, String)],
265 require_jwt: bool,
266 ) -> Result<Value> {
267 let resp = self
268 .raw_request(Method::GET, path, query, None, require_jwt)
269 .await?;
270 self.expect_success(resp, "GET", path)
271 }
272
273 async fn post_ok(
274 &self,
275 path: &str,
276 query: &[(&str, String)],
277 body: Value,
278 require_jwt: bool,
279 ) -> Result<Value> {
280 let resp = self
281 .raw_request(Method::POST, path, query, Some(body), require_jwt)
282 .await?;
283 self.expect_success(resp, "POST", path)
284 }
285
286 fn expect_success(&self, resp: RawApiResponse, method: &str, path: &str) -> Result<Value> {
287 if !resp.status.is_success() {
288 let body_str = resp
289 .json
290 .as_ref()
291 .map(|j| j.to_string())
292 .unwrap_or_default();
293 return Err(anyhow!(
294 "Predict API {} {} failed: status={} body={}",
295 method,
296 path,
297 resp.status,
298 &body_str[..body_str.len().min(500)]
299 ));
300 }
301
302 resp.json
303 .ok_or_else(|| anyhow!("Predict API {} {} returned non-JSON body", method, path))
304 }
305
306 async fn raw_request(
307 &self,
308 method: Method,
309 path: &str,
310 query: &[(&str, String)],
311 body: Option<Value>,
312 require_jwt: bool,
313 ) -> Result<RawApiResponse> {
314 if require_jwt && self.jwt.is_none() {
315 return Err(anyhow!(
316 "JWT required for {} {} but not set — call authenticate first",
317 method,
318 path
319 ));
320 }
321
322 let url = format!("{}{}", self.base, path);
323 let mut req = self
324 .http
325 .request(method.clone(), &url)
326 .header("Accept", "application/json")
327 .header("Content-Type", "application/json");
328
329 if !query.is_empty() {
330 req = req.query(query);
331 }
332
333 if let Some(api_key) = &self.api_key {
334 req = req.header("x-api-key", api_key);
335 }
336
337 if let Some(jwt) = &self.jwt {
338 req = req.header("Authorization", format!("Bearer {}", jwt));
339 }
340
341 if let Some(v) = body {
342 req = req.json(&v);
343 }
344
345 let resp = req
346 .send()
347 .await
348 .with_context(|| format!("predict api {} {} failed", method, path))?;
349
350 let status = resp.status();
351 let json = resp.json::<Value>().await.ok();
352
353 Ok(RawApiResponse { status, json })
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn client_construction() {
363 let client = PredictApiClient::new_mainnet("test-key").unwrap();
364 assert!(!client.has_jwt());
365
366 let client = client.with_jwt("token123");
367 assert!(client.has_jwt());
368 }
369}