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 all listings with builder pattern
150    pub fn get_all_listings(&self) -> ListingsRequestBuilder {
151        ListingsRequestBuilder::new(self)
152    }
153
154    /// Get specific listing
155    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    /// Get user's stall
161    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    /// Get inventory
167    pub async fn get_inventory(&self) -> Result<serde_json::Value> {
168        self.request(Method::GET, "/me/inventory", None::<&()>)
169            .await
170    }
171
172    /// Get watchlist
173    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    /// Get offers
179    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    /// Get trade history
185    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    /// Delete a listing
199    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    /// Delete a buy order
205    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    /// Delete from watchlist
211    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    /// Create a listing
217    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    /// Create a buy order
223    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    /// Make an offer
229    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    /// Buy now
240    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    /// Accept sales
250    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    /// Update listing price
257    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
265/// Builder for listings requests
266pub 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// Enums and request types
447
448#[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}