sagapay_sdk/
client.rs

1use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE};
2use reqwest::Client as ReqwestClient;
3use serde::{de::DeserializeOwned, Serialize};
4use std::sync::Arc;
5
6use crate::config::Config;
7use crate::error::Error;
8use crate::models::{
9    CreateDepositParams, CreateWithdrawalParams, DepositResponse, TransactionStatusResponse,
10    TransactionType, WalletBalanceResponse, WithdrawalResponse,
11};
12
13/// SagaPay API client
14#[derive(Debug, Clone)]
15pub struct Client {
16    config: Arc<Config>,
17    http_client: ReqwestClient,
18}
19
20impl Client {
21    /// Create a new SagaPay client
22    pub fn new(config: Config) -> Self {
23        let http_client = ReqwestClient::builder()
24            .timeout(std::time::Duration::from_secs(30))
25            .build()
26            .expect("Failed to create HTTP client");
27
28        Self {
29            config: Arc::new(config),
30            http_client,
31        }
32    }
33
34    /// Create a deposit address
35    ///
36    /// # Example
37    ///
38    /// ```no_run
39    /// use sagapay::{Client, Config, CreateDepositParams, NetworkType};
40    ///
41    /// #[tokio::main]
42    /// async fn main() -> Result<(), sagapay::Error> {
43    ///     let client = Client::new(Config {
44    ///         api_key: "your-api-key".to_string(),
45    ///         api_secret: "your-api-secret".to_string(),
46    ///         ..Config::default()
47    ///     });
48    ///
49    ///     let deposit = client.create_deposit(CreateDepositParams {
50    ///         network_type: NetworkType::BEP20,
51    ///         contract_address: "0".to_string(),
52    ///         amount: "1.5".to_string(),
53    ///         ipn_url: "https://example.com/webhook".to_string(),
54    ///         udf: Some("order-123".to_string()),
55    ///         address_type: Some("TEMPORARY".to_string()),
56    ///     }).await?;
57    ///
58    ///     println!("Deposit address: {}", deposit.address);
59    ///     Ok(())
60    /// }
61    /// ```
62    pub async fn create_deposit(
63        &self,
64        params: CreateDepositParams,
65    ) -> Result<DepositResponse, Error> {
66        self.post("/create-deposit", &params).await
67    }
68
69    /// Create a withdrawal
70    ///
71    /// # Example
72    ///
73    /// ```no_run
74    /// use sagapay::{Client, Config, CreateWithdrawalParams, NetworkType};
75    ///
76    /// #[tokio::main]
77    /// async fn main() -> Result<(), sagapay::Error> {
78    ///     let client = Client::new(Config {
79    ///         api_key: "your-api-key".to_string(),
80    ///         api_secret: "your-api-secret".to_string(),
81    ///         ..Config::default()
82    ///     });
83    ///
84    ///     let withdrawal = client.create_withdrawal(CreateWithdrawalParams {
85    ///         network_type: NetworkType::ERC20,
86    ///         contract_address: "0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string(), // USDT
87    ///         address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e".to_string(),
88    ///         amount: "10.5".to_string(),
89    ///         ipn_url: "https://example.com/webhook".to_string(),
90    ///         udf: Some("withdrawal-456".to_string()),
91    ///     }).await?;
92    ///
93    ///     println!("Withdrawal ID: {}", withdrawal.id);
94    ///     Ok(())
95    /// }
96    /// ```
97    pub async fn create_withdrawal(
98        &self,
99        params: CreateWithdrawalParams,
100    ) -> Result<WithdrawalResponse, Error> {
101        self.post("/create-withdrawal", &params).await
102    }
103
104    /// Check transaction status
105    ///
106    /// # Example
107    ///
108    /// ```no_run
109    /// use sagapay::{Client, Config, TransactionType};
110    ///
111    /// #[tokio::main]
112    /// async fn main() -> Result<(), sagapay::Error> {
113    ///     let client = Client::new(Config {
114    ///         api_key: "your-api-key".to_string(),
115    ///         api_secret: "your-api-secret".to_string(),
116    ///         ..Config::default()
117    ///     });
118    ///
119    ///     let address = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
120    ///     let tx_type = TransactionType::Deposit;
121    ///     
122    ///     let status = client.check_transaction_status(address, tx_type).await?;
123    ///     
124    ///     println!("Transaction count: {}", status.count);
125    ///     for tx in status.transactions {
126    ///         println!("ID: {}, Status: {:?}, Amount: {}", tx.id, tx.status, tx.amount);
127    ///     }
128    ///     
129    ///     Ok(())
130    /// }
131    /// ```
132    pub async fn check_transaction_status(
133        &self,
134        address: &str,
135        transaction_type: TransactionType,
136    ) -> Result<TransactionStatusResponse, Error> {
137        let tx_type = match transaction_type {
138            TransactionType::Deposit => "deposit",
139            TransactionType::Withdrawal => "withdrawal",
140        };
141
142        let params = [("address", address), ("type", tx_type)];
143
144        self.get_with_query("/check-transaction-status", &params)
145            .await
146    }
147
148    /// Fetch wallet balance
149    ///
150    /// # Example
151    ///
152    /// ```no_run
153    /// use sagapay::{Client, Config, NetworkType};
154    ///
155    /// #[tokio::main]
156    /// async fn main() -> Result<(), sagapay::Error> {
157    ///     let client = Client::new(Config {
158    ///         api_key: "your-api-key".to_string(),
159    ///         api_secret: "your-api-secret".to_string(),
160    ///         ..Config::default()
161    ///     });
162    ///
163    ///     let address = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";
164    ///     let network_type = NetworkType::ERC20;
165    ///     let contract_address = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; // USDT
166    ///     
167    ///     let balance = client.fetch_wallet_balance(address, network_type, contract_address).await?;
168    ///     
169    ///     println!("Token: {} ({})", balance.token.symbol, balance.token.name);
170    ///     println!("Balance: {}", balance.balance.formatted);
171    ///     
172    ///     Ok(())
173    /// }
174    /// ```
175    pub async fn fetch_wallet_balance(
176        &self,
177        address: &str,
178        network_type: crate::models::NetworkType,
179        contract_address: &str,
180    ) -> Result<WalletBalanceResponse, Error> {
181        let params = [
182            ("address", address),
183            ("networkType", &network_type.to_string()),
184            ("contractAddress", contract_address),
185        ];
186
187        self.get_with_query("/fetch-wallet-balance", &params).await
188    }
189
190    /// Get the client's API key
191    pub fn api_key(&self) -> &str {
192        &self.config.api_key
193    }
194
195    /// Get the client's API secret
196    pub fn api_secret(&self) -> &str {
197        &self.config.api_secret
198    }
199
200    /// Make a GET request
201    async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
202        let url = format!("{}{}", self.config.base_url, path);
203        let headers = self.build_headers()?;
204
205        let response = self.http_client.get(&url).headers(headers).send().await?;
206
207        self.handle_response(response).await
208    }
209
210    /// Make a GET request with query parameters
211    async fn get_with_query<T: DeserializeOwned, Q: Serialize + ?Sized>(
212        &self,
213        path: &str,
214        query: &Q,
215    ) -> Result<T, Error> {
216        let url = format!("{}{}", self.config.base_url, path);
217        let headers = self.build_headers()?;
218
219        let response = self
220            .http_client
221            .get(&url)
222            .headers(headers)
223            .query(query)
224            .send()
225            .await?;
226
227        self.handle_response(response).await
228    }
229
230    /// Make a POST request
231    async fn post<T: DeserializeOwned, D: Serialize + ?Sized>(
232        &self,
233        path: &str,
234        data: &D,
235    ) -> Result<T, Error> {
236        let url = format!("{}{}", self.config.base_url, path);
237        let headers = self.build_headers()?;
238
239        let response = self
240            .http_client
241            .post(&url)
242            .headers(headers)
243            .json(data)
244            .send()
245            .await?;
246
247        self.handle_response(response).await
248    }
249
250    /// Build common headers for requests
251    fn build_headers(&self) -> Result<HeaderMap, Error> {
252        let mut headers = HeaderMap::new();
253
254        headers.insert(
255            "x-api-key",
256            HeaderValue::from_str(&self.config.api_key).map_err(|e| Error::Other(e.to_string()))?,
257        );
258        headers.insert(
259            "x-api-secret",
260            HeaderValue::from_str(&self.config.api_secret)
261                .map_err(|e| Error::Other(e.to_string()))?,
262        );
263        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
264        headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
265
266        Ok(headers)
267    }
268
269    /// Handle API response
270    async fn handle_response<T: DeserializeOwned>(
271        &self,
272        response: reqwest::Response,
273    ) -> Result<T, Error> {
274        let status = response.status();
275        let body = response.text().await?;
276
277        if !status.is_success() {
278            let error_response: serde_json::Value = match serde_json::from_str(&body) {
279                Ok(json) => json,
280                Err(_) => {
281                    return Err(Error::ApiError {
282                        status_code: status.as_u16(),
283                        message: body,
284                        error_code: None,
285                    });
286                }
287            };
288
289            let message = error_response
290                .get("message")
291                .and_then(|m| m.as_str())
292                .unwrap_or("Unknown error")
293                .to_string();
294
295            let error_code = error_response
296                .get("error")
297                .and_then(|e| e.as_str())
298                .map(|s| s.to_string());
299
300            return Err(Error::ApiError {
301                status_code: status.as_u16(),
302                message,
303                error_code,
304            });
305        }
306
307        match serde_json::from_str(&body) {
308            Ok(data) => Ok(data),
309            Err(e) => Err(Error::JsonError(e)),
310        }
311    }
312}