1use crate::error::{Error, Result};
2use crate::models::*;
3use reqwest::{Client as HttpClient, Method, Proxy};
4use serde::{Deserialize, Serialize};
5
6const API_URL: &str = "https://csfloat.com/api/v1";
7
8pub struct Client {
10 http_client: HttpClient,
11 api_key: String,
12}
13
14impl Client {
15 pub fn new(api_key: impl Into<String>) -> Result<Self> {
17 let http_client = HttpClient::builder()
18 .build()
19 .map_err(Error::RequestError)?;
20
21 Ok(Self {
22 http_client,
23 api_key: api_key.into(),
24 })
25 }
26
27 pub fn with_proxy(api_key: impl Into<String>, proxy_url: impl AsRef<str>) -> Result<Self> {
29 let proxy = Proxy::all(proxy_url.as_ref())
30 .map_err(|_| Error::InvalidProxy(proxy_url.as_ref().to_string()))?;
31
32 let http_client = HttpClient::builder()
33 .proxy(proxy)
34 .build()
35 .map_err(Error::RequestError)?;
36
37 Ok(Self {
38 http_client,
39 api_key: api_key.into(),
40 })
41 }
42
43 async fn request<T: for<'de> Deserialize<'de>>(
45 &self,
46 method: Method,
47 path: &str,
48 json_body: Option<&impl Serialize>,
49 ) -> Result<T> {
50 let url = format!("{API_URL}{path}");
51
52 let mut request = self
53 .http_client
54 .request(method, &url)
55 .header("Authorization", &self.api_key);
56
57 if let Some(body) = json_body {
58 request = request.json(body);
59 }
60
61 let response = request.send().await?;
62 let status = response.status();
63
64 if !status.is_success() {
65 let body = response.text().await.unwrap_or_default();
66 return Err(Error::from_status(status.as_u16(), body));
67 }
68
69 let content_type = response
70 .headers()
71 .get("content-type")
72 .and_then(|v| v.to_str().ok())
73 .unwrap_or("");
74
75 if !content_type.contains("application/json") {
76 return Err(Error::UnexpectedContentType(content_type.to_string()));
77 }
78
79 let data = response.json().await?;
80 Ok(data)
81 }
82
83 pub async fn get_exchange_rates(&self) -> Result<ExchangeRates> {
85 self.request(Method::GET, "/meta/exchange-rates", None::<&()>)
86 .await
87 }
88
89 pub async fn get_me(&self) -> Result<Me> {
91 self.request(Method::GET, "/me", None::<&()>).await
92 }
93
94 pub async fn get_location(&self) -> Result<serde_json::Value> {
96 self.request(Method::GET, "/meta/location", None::<&()>)
97 .await
98 }
99
100 pub async fn get_transactions(&self, page: u32, limit: u32) -> Result<serde_json::Value> {
102 let path = format!("/me/transactions?page={page}&limit={limit}&order=desc");
103 self.request(Method::GET, &path, None::<&()>).await
104 }
105
106 pub async fn get_account_standing(&self) -> Result<serde_json::Value> {
108 self.request(Method::GET, "/me/account-standing", None::<&()>)
109 .await
110 }
111
112 pub async fn get_pending_trades(&self, limit: u32, page: u32) -> Result<serde_json::Value> {
114 let path = format!("/me/trades?state=pending&limit={limit}&page={page}");
115 self.request(Method::GET, &path, None::<&()>).await
116 }
117
118 pub async fn get_similar(&self, listing_id: &str) -> Result<Vec<Listing>> {
120 let path = format!("/listings/{listing_id}/similar");
121 self.request(Method::GET, &path, None::<&()>).await
122 }
123
124 pub async fn get_buy_orders(&self, listing_id: &str, limit: u32) -> Result<Vec<BuyOrder>> {
126 let path = format!("/listings/{listing_id}/buy-orders?limit={limit}");
127 self.request(Method::GET, &path, None::<&()>).await
128 }
129
130 pub async fn get_my_buy_orders(&self, page: u32, limit: u32) -> Result<serde_json::Value> {
132 let path = format!("/me/buy-orders?page={page}&limit={limit}&order=desc");
133 self.request(Method::GET, &path, None::<&()>).await
134 }
135
136 pub async fn get_sales(
138 &self,
139 market_hash_name: &str,
140 paint_index: Option<i32>,
141 ) -> Result<serde_json::Value> {
142 let mut path = format!("/history/{market_hash_name}/sales");
143 if let Some(idx) = paint_index {
144 path.push_str(&format!("?paint_index={idx}"));
145 }
146 self.request(Method::GET, &path, None::<&()>).await
147 }
148
149 pub async fn get_schema(&self) -> Result<SchemaResponse> {
152 self.request(Method::GET, "/schema", None::<&()>).await
153 }
154
155 pub fn get_all_listings(&self) -> ListingsRequestBuilder {
157 ListingsRequestBuilder::new(self)
158 }
159
160 pub async fn get_specific_listing(&self, listing_id: &str) -> Result<Listing> {
162 let path = format!("/listings/{listing_id}");
163 self.request(Method::GET, &path, None::<&()>).await
164 }
165
166 pub async fn get_stall(&self, user_id: &str, limit: u32) -> Result<Stall> {
168 let path = format!("/users/{user_id}/stall?limit={limit}");
169 self.request(Method::GET, &path, None::<&()>).await
170 }
171
172 pub async fn get_inventory(&self) -> Result<serde_json::Value> {
174 self.request(Method::GET, "/me/inventory", None::<&()>)
175 .await
176 }
177
178 pub async fn get_watchlist(&self, limit: u32) -> Result<serde_json::Value> {
180 let path = format!("/me/watchlist?limit={limit}");
181 self.request(Method::GET, &path, None::<&()>).await
182 }
183
184 pub async fn get_offers(&self, limit: u32) -> Result<serde_json::Value> {
186 let path = format!("/me/offers-timeline?limit={limit}");
187 self.request(Method::GET, &path, None::<&()>).await
188 }
189
190 pub async fn get_trade_history(
192 &self,
193 role: TradeRole,
194 limit: u32,
195 page: u32,
196 ) -> Result<serde_json::Value> {
197 let role_str = role.as_str();
198 let path = format!(
199 "/me/trades?role={role_str}&state=failed,cancelled,verified&limit={limit}&page={page}"
200 );
201 self.request(Method::GET, &path, None::<&()>).await
202 }
203
204 pub async fn delete_listing(&self, listing_id: &str) -> Result<serde_json::Value> {
206 let path = format!("/listings/{listing_id}");
207 self.request(Method::DELETE, &path, None::<&()>).await
208 }
209
210 pub async fn delete_buy_order(&self, id: &str) -> Result<serde_json::Value> {
212 let path = format!("/buy-orders/{id}");
213 self.request(Method::DELETE, &path, None::<&()>).await
214 }
215
216 pub async fn delete_watchlist(&self, id: i64) -> Result<serde_json::Value> {
218 let path = format!("/listings/{id}/watchlist");
219 self.request(Method::DELETE, &path, None::<&()>).await
220 }
221
222 pub async fn create_listing(&self, request: CreateListingRequest) -> Result<serde_json::Value> {
224 self.request(Method::POST, "/listings", Some(&request))
225 .await
226 }
227
228 pub async fn create_buy_order(&self, request: CreateBuyOrderRequest) -> Result<serde_json::Value> {
230 self.request(Method::POST, "/buy-orders", Some(&request))
231 .await
232 }
233
234 pub async fn make_offer(&self, listing_id: &str, price: i32) -> Result<serde_json::Value> {
236 let request = MakeOfferRequest {
237 contract_id: listing_id.to_string(),
238 price,
239 cancel_previous_offer: false,
240 };
241 self.request(Method::POST, "/offers", Some(&request))
242 .await
243 }
244
245 pub async fn buy_now(&self, total_price: i32, listing_id: &str) -> Result<serde_json::Value> {
247 let request = BuyNowRequest {
248 total_price,
249 contract_ids: vec![listing_id.to_string()],
250 };
251 self.request(Method::POST, "/listings/buy", Some(&request))
252 .await
253 }
254
255 pub async fn accept_sale(&self, trade_ids: Vec<String>) -> Result<serde_json::Value> {
257 let request = AcceptSaleRequest { trade_ids };
258 self.request(Method::POST, "/trades/bulk/accept", Some(&request))
259 .await
260 }
261
262 pub async fn update_listing_price(&self, listing_id: &str, price: i32) -> Result<serde_json::Value> {
264 let request = UpdatePriceRequest { price };
265 let path = format!("/listings/{listing_id}");
266 self.request(Method::PATCH, &path, Some(&request))
267 .await
268 }
269}
270
271pub struct ListingsRequestBuilder<'a> {
273 client: &'a Client,
274 min_price: Option<i32>,
275 max_price: Option<i32>,
276 cursor: Option<String>,
277 limit: u32,
278 sort_by: SortBy,
279 category: Category,
280 def_index: Option<Vec<i32>>,
281 min_float: Option<f64>,
282 max_float: Option<f64>,
283 rarity: Option<String>,
284 paint_seed: Option<i32>,
285 paint_index: Option<i32>,
286 user_id: Option<String>,
287 collection: Option<String>,
288 market_hash_name: Option<String>,
289 listing_type: ListingType,
290}
291
292impl<'a> ListingsRequestBuilder<'a> {
293 fn new(client: &'a Client) -> Self {
294 Self {
295 client,
296 min_price: None,
297 max_price: None,
298 cursor: None,
299 limit: 50,
300 sort_by: SortBy::BestDeal,
301 category: Category::Any,
302 def_index: None,
303 min_float: None,
304 max_float: None,
305 rarity: None,
306 paint_seed: None,
307 paint_index: None,
308 user_id: None,
309 collection: None,
310 market_hash_name: None,
311 listing_type: ListingType::BuyNow,
312 }
313 }
314
315 pub fn min_price(mut self, price: i32) -> Self {
316 self.min_price = Some(price);
317 self
318 }
319
320 pub fn max_price(mut self, price: i32) -> Self {
321 self.max_price = Some(price);
322 self
323 }
324
325 pub fn cursor(mut self, cursor: String) -> Self {
326 self.cursor = Some(cursor);
327 self
328 }
329
330 pub fn limit(mut self, limit: u32) -> Self {
331 self.limit = limit;
332 self
333 }
334
335 pub fn sort_by(mut self, sort: SortBy) -> Self {
336 self.sort_by = sort;
337 self
338 }
339
340 pub fn category(mut self, cat: Category) -> Self {
341 self.category = cat;
342 self
343 }
344
345 pub fn def_index(mut self, indices: Vec<i32>) -> Self {
346 self.def_index = Some(indices);
347 self
348 }
349
350 pub fn min_float(mut self, float: f64) -> Self {
351 self.min_float = Some(float);
352 self
353 }
354
355 pub fn max_float(mut self, float: f64) -> Self {
356 self.max_float = Some(float);
357 self
358 }
359
360 pub fn rarity(mut self, rarity: String) -> Self {
361 self.rarity = Some(rarity);
362 self
363 }
364
365 pub fn paint_seed(mut self, seed: i32) -> Self {
366 self.paint_seed = Some(seed);
367 self
368 }
369
370 pub fn paint_index(mut self, index: i32) -> Self {
371 self.paint_index = Some(index);
372 self
373 }
374
375 pub fn user_id(mut self, id: String) -> Self {
376 self.user_id = Some(id);
377 self
378 }
379
380 pub fn collection(mut self, collection: String) -> Self {
381 self.collection = Some(collection);
382 self
383 }
384
385 pub fn market_hash_name(mut self, name: String) -> Self {
386 self.market_hash_name = Some(name);
387 self
388 }
389
390 pub fn listing_type(mut self, t: ListingType) -> Self {
391 self.listing_type = t;
392 self
393 }
394
395 pub async fn send(self) -> Result<ListingsResponse> {
396 let mut path = format!(
397 "/listings?limit={}&sort_by={}&category={}&type={}",
398 self.limit,
399 self.sort_by.as_str(),
400 self.category.as_u8(),
401 self.listing_type.as_str()
402 );
403
404 if let Some(cursor) = &self.cursor {
405 path.push_str(&format!("&cursor={cursor}"));
406 }
407 if let Some(min) = self.min_price {
408 path.push_str(&format!("&min_price={min}"));
409 }
410 if let Some(max) = self.max_price {
411 path.push_str(&format!("&max_price={max}"));
412 }
413 if let Some(indices) = &self.def_index {
414 let indices_str = indices
415 .iter()
416 .map(|i| i.to_string())
417 .collect::<Vec<_>>()
418 .join(",");
419 path.push_str(&format!("&def_index={indices_str}"));
420 }
421 if let Some(min) = self.min_float {
422 path.push_str(&format!("&min_float={min}"));
423 }
424 if let Some(max) = self.max_float {
425 path.push_str(&format!("&max_float={max}"));
426 }
427 if let Some(r) = &self.rarity {
428 path.push_str(&format!("&rarity={r}"));
429 }
430 if let Some(seed) = self.paint_seed {
431 path.push_str(&format!("&paint_seed={seed}"));
432 }
433 if let Some(idx) = self.paint_index {
434 path.push_str(&format!("&paint_index={idx}"));
435 }
436 if let Some(id) = &self.user_id {
437 path.push_str(&format!("&user_id={id}"));
438 }
439 if let Some(col) = &self.collection {
440 path.push_str(&format!("&collection={col}"));
441 }
442 if let Some(name) = &self.market_hash_name {
443 path.push_str(&format!("&market_hash_name={name}"));
444 }
445
446 self.client
447 .request(Method::GET, &path, None::<&()>)
448 .await
449 }
450}
451
452#[derive(Debug, Clone, Copy)]
455pub enum SortBy {
456 LowestPrice,
457 HighestPrice,
458 MostRecent,
459 ExpiresSoon,
460 LowestFloat,
461 HighestFloat,
462 BestDeal,
463 HighestDiscount,
464 FloatRank,
465 NumBids,
466}
467
468impl SortBy {
469 fn as_str(&self) -> &str {
470 match self {
471 Self::LowestPrice => "lowest_price",
472 Self::HighestPrice => "highest_price",
473 Self::MostRecent => "most_recent",
474 Self::ExpiresSoon => "expires_soon",
475 Self::LowestFloat => "lowest_float",
476 Self::HighestFloat => "highest_float",
477 Self::BestDeal => "best_deal",
478 Self::HighestDiscount => "highest_discount",
479 Self::FloatRank => "float_rank",
480 Self::NumBids => "num_bids",
481 }
482 }
483}
484
485#[derive(Debug, Clone, Copy)]
486pub enum Category {
487 Any = 0,
488 Normal = 1,
489 StatTrak = 2,
490 Souvenir = 3,
491}
492
493impl Category {
494 fn as_u8(&self) -> u8 {
495 *self as u8
496 }
497}
498
499#[derive(Debug, Clone, Copy)]
500pub enum ListingType {
501 BuyNow,
502 Auction,
503}
504
505impl ListingType {
506 fn as_str(&self) -> &str {
507 match self {
508 Self::BuyNow => "buy_now",
509 Self::Auction => "auction",
510 }
511 }
512}
513
514#[derive(Debug, Clone, Copy)]
515pub enum TradeRole {
516 Seller,
517 Buyer,
518}
519
520impl TradeRole {
521 fn as_str(&self) -> &str {
522 match self {
523 Self::Seller => "seller",
524 Self::Buyer => "buyer",
525 }
526 }
527}
528
529#[derive(Debug, Clone, Serialize)]
530pub struct CreateListingRequest {
531 pub asset_id: String,
532 pub price: f64,
533 #[serde(rename = "type")]
534 pub listing_type: String,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub max_offer_discount: Option<i32>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub reserve_price: Option<f64>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub duration_days: Option<i32>,
541 pub description: String,
542 pub private: bool,
543}
544
545#[derive(Debug, Clone, Serialize)]
546pub struct CreateBuyOrderRequest {
547 pub market_hash_name: String,
548 pub max_price: i32,
549 pub quantity: i32,
550}
551
552#[derive(Debug, Clone, Serialize)]
553struct MakeOfferRequest {
554 contract_id: String,
555 price: i32,
556 cancel_previous_offer: bool,
557}
558
559#[derive(Debug, Clone, Serialize)]
560struct BuyNowRequest {
561 total_price: i32,
562 contract_ids: Vec<String>,
563}
564
565#[derive(Debug, Clone, Serialize)]
566struct AcceptSaleRequest {
567 trade_ids: Vec<String>,
568}
569
570#[derive(Debug, Clone, Serialize)]
571struct UpdatePriceRequest {
572 price: i32,
573}