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", ¶ms).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", ¶ms).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", ¶ms)
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", ¶ms).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}