csfloat_rs/
client.rs

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
8/// CSFloat API Client
9pub struct Client {
10    http_client: HttpClient,
11    api_key: String,
12}
13
14impl Client {
15    /// Create a new CSFloat client with API key
16    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    /// Create a new CSFloat client with API key and proxy
28    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    /// Internal request method
44    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    /// Get exchange rates
84    pub async fn get_exchange_rates(&self) -> Result<ExchangeRates> {
85        self.request(Method::GET, "/meta/exchange-rates", None::<&()>)
86            .await
87    }
88
89    /// Get authenticated user profile
90    pub async fn get_me(&self) -> Result<Me> {
91        self.request(Method::GET, "/me", None::<&()>).await
92    }
93
94    /// Get user's location
95    pub async fn get_location(&self) -> Result<serde_json::Value> {
96        self.request(Method::GET, "/meta/location", None::<&()>)
97            .await
98    }
99
100    /// Get transactions
101    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    /// Get account standing
107    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    /// Get pending trades
113    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    /// Get similar listings
119    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    /// Get buy orders for a listing
125    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    /// Get user's own buy orders
131    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    /// Get sales history
137    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    /// Get schema with weapon pricing data
150    /// This endpoint returns average prices for each skin in different wear conditions
151    pub async fn get_schema(&self) -> Result<SchemaResponse> {
152        self.request(Method::GET, "/schema", None::<&()>).await
153    }
154
155    /// Get all listings with builder pattern
156    pub fn get_all_listings(&self) -> ListingsRequestBuilder {
157        ListingsRequestBuilder::new(self)
158    }
159
160    /// Get specific listing
161    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    /// Get user's stall
167    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    /// Get inventory
173    pub async fn get_inventory(&self) -> Result<serde_json::Value> {
174        self.request(Method::GET, "/me/inventory", None::<&()>)
175            .await
176    }
177
178    /// Get watchlist
179    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    /// Get offers
185    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    /// Get trade history
191    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    /// Delete a listing
205    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    /// Delete a buy order
211    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    /// Delete from watchlist
217    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    /// Create a listing
223    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    /// Create a buy order
229    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    /// Make an offer
235    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    /// Buy now
246    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    /// Accept sales
256    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    /// Update listing price
263    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
271/// Builder for listings requests
272pub 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// Enums and request types
453
454#[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}