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 fn get_all_listings(&self) -> ListingsRequestBuilder {
151 ListingsRequestBuilder::new(self)
152 }
153
154 pub async fn get_specific_listing(&self, listing_id: &str) -> Result<Listing> {
156 let path = format!("/listings/{listing_id}");
157 self.request(Method::GET, &path, None::<&()>).await
158 }
159
160 pub async fn get_stall(&self, user_id: &str, limit: u32) -> Result<Stall> {
162 let path = format!("/users/{user_id}/stall?limit={limit}");
163 self.request(Method::GET, &path, None::<&()>).await
164 }
165
166 pub async fn get_inventory(&self) -> Result<serde_json::Value> {
168 self.request(Method::GET, "/me/inventory", None::<&()>)
169 .await
170 }
171
172 pub async fn get_watchlist(&self, limit: u32) -> Result<serde_json::Value> {
174 let path = format!("/me/watchlist?limit={limit}");
175 self.request(Method::GET, &path, None::<&()>).await
176 }
177
178 pub async fn get_offers(&self, limit: u32) -> Result<serde_json::Value> {
180 let path = format!("/me/offers-timeline?limit={limit}");
181 self.request(Method::GET, &path, None::<&()>).await
182 }
183
184 pub async fn get_trade_history(
186 &self,
187 role: TradeRole,
188 limit: u32,
189 page: u32,
190 ) -> Result<serde_json::Value> {
191 let role_str = role.as_str();
192 let path = format!(
193 "/me/trades?role={role_str}&state=failed,cancelled,verified&limit={limit}&page={page}"
194 );
195 self.request(Method::GET, &path, None::<&()>).await
196 }
197
198 pub async fn delete_listing(&self, listing_id: &str) -> Result<serde_json::Value> {
200 let path = format!("/listings/{listing_id}");
201 self.request(Method::DELETE, &path, None::<&()>).await
202 }
203
204 pub async fn delete_buy_order(&self, id: &str) -> Result<serde_json::Value> {
206 let path = format!("/buy-orders/{id}");
207 self.request(Method::DELETE, &path, None::<&()>).await
208 }
209
210 pub async fn delete_watchlist(&self, id: i64) -> Result<serde_json::Value> {
212 let path = format!("/listings/{id}/watchlist");
213 self.request(Method::DELETE, &path, None::<&()>).await
214 }
215
216 pub async fn create_listing(&self, request: CreateListingRequest) -> Result<serde_json::Value> {
218 self.request(Method::POST, "/listings", Some(&request))
219 .await
220 }
221
222 pub async fn create_buy_order(&self, request: CreateBuyOrderRequest) -> Result<serde_json::Value> {
224 self.request(Method::POST, "/buy-orders", Some(&request))
225 .await
226 }
227
228 pub async fn make_offer(&self, listing_id: &str, price: i32) -> Result<serde_json::Value> {
230 let request = MakeOfferRequest {
231 contract_id: listing_id.to_string(),
232 price,
233 cancel_previous_offer: false,
234 };
235 self.request(Method::POST, "/offers", Some(&request))
236 .await
237 }
238
239 pub async fn buy_now(&self, total_price: i32, listing_id: &str) -> Result<serde_json::Value> {
241 let request = BuyNowRequest {
242 total_price,
243 contract_ids: vec![listing_id.to_string()],
244 };
245 self.request(Method::POST, "/listings/buy", Some(&request))
246 .await
247 }
248
249 pub async fn accept_sale(&self, trade_ids: Vec<String>) -> Result<serde_json::Value> {
251 let request = AcceptSaleRequest { trade_ids };
252 self.request(Method::POST, "/trades/bulk/accept", Some(&request))
253 .await
254 }
255
256 pub async fn update_listing_price(&self, listing_id: &str, price: i32) -> Result<serde_json::Value> {
258 let request = UpdatePriceRequest { price };
259 let path = format!("/listings/{listing_id}");
260 self.request(Method::PATCH, &path, Some(&request))
261 .await
262 }
263}
264
265pub struct ListingsRequestBuilder<'a> {
267 client: &'a Client,
268 min_price: Option<i32>,
269 max_price: Option<i32>,
270 cursor: Option<String>,
271 limit: u32,
272 sort_by: SortBy,
273 category: Category,
274 def_index: Option<Vec<i32>>,
275 min_float: Option<f64>,
276 max_float: Option<f64>,
277 rarity: Option<String>,
278 paint_seed: Option<i32>,
279 paint_index: Option<i32>,
280 user_id: Option<String>,
281 collection: Option<String>,
282 market_hash_name: Option<String>,
283 listing_type: ListingType,
284}
285
286impl<'a> ListingsRequestBuilder<'a> {
287 fn new(client: &'a Client) -> Self {
288 Self {
289 client,
290 min_price: None,
291 max_price: None,
292 cursor: None,
293 limit: 50,
294 sort_by: SortBy::BestDeal,
295 category: Category::Any,
296 def_index: None,
297 min_float: None,
298 max_float: None,
299 rarity: None,
300 paint_seed: None,
301 paint_index: None,
302 user_id: None,
303 collection: None,
304 market_hash_name: None,
305 listing_type: ListingType::BuyNow,
306 }
307 }
308
309 pub fn min_price(mut self, price: i32) -> Self {
310 self.min_price = Some(price);
311 self
312 }
313
314 pub fn max_price(mut self, price: i32) -> Self {
315 self.max_price = Some(price);
316 self
317 }
318
319 pub fn cursor(mut self, cursor: String) -> Self {
320 self.cursor = Some(cursor);
321 self
322 }
323
324 pub fn limit(mut self, limit: u32) -> Self {
325 self.limit = limit;
326 self
327 }
328
329 pub fn sort_by(mut self, sort: SortBy) -> Self {
330 self.sort_by = sort;
331 self
332 }
333
334 pub fn category(mut self, cat: Category) -> Self {
335 self.category = cat;
336 self
337 }
338
339 pub fn def_index(mut self, indices: Vec<i32>) -> Self {
340 self.def_index = Some(indices);
341 self
342 }
343
344 pub fn min_float(mut self, float: f64) -> Self {
345 self.min_float = Some(float);
346 self
347 }
348
349 pub fn max_float(mut self, float: f64) -> Self {
350 self.max_float = Some(float);
351 self
352 }
353
354 pub fn rarity(mut self, rarity: String) -> Self {
355 self.rarity = Some(rarity);
356 self
357 }
358
359 pub fn paint_seed(mut self, seed: i32) -> Self {
360 self.paint_seed = Some(seed);
361 self
362 }
363
364 pub fn paint_index(mut self, index: i32) -> Self {
365 self.paint_index = Some(index);
366 self
367 }
368
369 pub fn user_id(mut self, id: String) -> Self {
370 self.user_id = Some(id);
371 self
372 }
373
374 pub fn collection(mut self, collection: String) -> Self {
375 self.collection = Some(collection);
376 self
377 }
378
379 pub fn market_hash_name(mut self, name: String) -> Self {
380 self.market_hash_name = Some(name);
381 self
382 }
383
384 pub fn listing_type(mut self, t: ListingType) -> Self {
385 self.listing_type = t;
386 self
387 }
388
389 pub async fn send(self) -> Result<ListingsResponse> {
390 let mut path = format!(
391 "/listings?limit={}&sort_by={}&category={}&type={}",
392 self.limit,
393 self.sort_by.as_str(),
394 self.category.as_u8(),
395 self.listing_type.as_str()
396 );
397
398 if let Some(cursor) = &self.cursor {
399 path.push_str(&format!("&cursor={cursor}"));
400 }
401 if let Some(min) = self.min_price {
402 path.push_str(&format!("&min_price={min}"));
403 }
404 if let Some(max) = self.max_price {
405 path.push_str(&format!("&max_price={max}"));
406 }
407 if let Some(indices) = &self.def_index {
408 let indices_str = indices
409 .iter()
410 .map(|i| i.to_string())
411 .collect::<Vec<_>>()
412 .join(",");
413 path.push_str(&format!("&def_index={indices_str}"));
414 }
415 if let Some(min) = self.min_float {
416 path.push_str(&format!("&min_float={min}"));
417 }
418 if let Some(max) = self.max_float {
419 path.push_str(&format!("&max_float={max}"));
420 }
421 if let Some(r) = &self.rarity {
422 path.push_str(&format!("&rarity={r}"));
423 }
424 if let Some(seed) = self.paint_seed {
425 path.push_str(&format!("&paint_seed={seed}"));
426 }
427 if let Some(idx) = self.paint_index {
428 path.push_str(&format!("&paint_index={idx}"));
429 }
430 if let Some(id) = &self.user_id {
431 path.push_str(&format!("&user_id={id}"));
432 }
433 if let Some(col) = &self.collection {
434 path.push_str(&format!("&collection={col}"));
435 }
436 if let Some(name) = &self.market_hash_name {
437 path.push_str(&format!("&market_hash_name={name}"));
438 }
439
440 self.client
441 .request(Method::GET, &path, None::<&()>)
442 .await
443 }
444}
445
446#[derive(Debug, Clone, Copy)]
449pub enum SortBy {
450 LowestPrice,
451 HighestPrice,
452 MostRecent,
453 ExpiresSoon,
454 LowestFloat,
455 HighestFloat,
456 BestDeal,
457 HighestDiscount,
458 FloatRank,
459 NumBids,
460}
461
462impl SortBy {
463 fn as_str(&self) -> &str {
464 match self {
465 Self::LowestPrice => "lowest_price",
466 Self::HighestPrice => "highest_price",
467 Self::MostRecent => "most_recent",
468 Self::ExpiresSoon => "expires_soon",
469 Self::LowestFloat => "lowest_float",
470 Self::HighestFloat => "highest_float",
471 Self::BestDeal => "best_deal",
472 Self::HighestDiscount => "highest_discount",
473 Self::FloatRank => "float_rank",
474 Self::NumBids => "num_bids",
475 }
476 }
477}
478
479#[derive(Debug, Clone, Copy)]
480pub enum Category {
481 Any = 0,
482 Normal = 1,
483 StatTrak = 2,
484 Souvenir = 3,
485}
486
487impl Category {
488 fn as_u8(&self) -> u8 {
489 *self as u8
490 }
491}
492
493#[derive(Debug, Clone, Copy)]
494pub enum ListingType {
495 BuyNow,
496 Auction,
497}
498
499impl ListingType {
500 fn as_str(&self) -> &str {
501 match self {
502 Self::BuyNow => "buy_now",
503 Self::Auction => "auction",
504 }
505 }
506}
507
508#[derive(Debug, Clone, Copy)]
509pub enum TradeRole {
510 Seller,
511 Buyer,
512}
513
514impl TradeRole {
515 fn as_str(&self) -> &str {
516 match self {
517 Self::Seller => "seller",
518 Self::Buyer => "buyer",
519 }
520 }
521}
522
523#[derive(Debug, Clone, Serialize)]
524pub struct CreateListingRequest {
525 pub asset_id: String,
526 pub price: f64,
527 #[serde(rename = "type")]
528 pub listing_type: String,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub max_offer_discount: Option<i32>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub reserve_price: Option<f64>,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub duration_days: Option<i32>,
535 pub description: String,
536 pub private: bool,
537}
538
539#[derive(Debug, Clone, Serialize)]
540pub struct CreateBuyOrderRequest {
541 pub market_hash_name: String,
542 pub max_price: i32,
543 pub quantity: i32,
544}
545
546#[derive(Debug, Clone, Serialize)]
547struct MakeOfferRequest {
548 contract_id: String,
549 price: i32,
550 cancel_previous_offer: bool,
551}
552
553#[derive(Debug, Clone, Serialize)]
554struct BuyNowRequest {
555 total_price: i32,
556 contract_ids: Vec<String>,
557}
558
559#[derive(Debug, Clone, Serialize)]
560struct AcceptSaleRequest {
561 trade_ids: Vec<String>,
562}
563
564#[derive(Debug, Clone, Serialize)]
565struct UpdatePriceRequest {
566 price: i32,
567}