1pub mod charges;
2mod custom_deserializer;
3pub mod email;
4pub mod errors;
5pub mod gamertag;
6pub mod internal_transfer;
7pub mod keysend;
8pub mod ln_address;
9pub mod login_with_zbd;
10mod models;
11pub mod payments;
12pub mod utilities;
13pub mod voucher;
14pub mod wallet;
15pub mod withdrawal_request;
16
17use std::borrow::Cow;
18
19use charges::*;
20use email::*;
21use errors::*;
22use gamertag::*;
23use internal_transfer::*;
24use keysend::*;
25use ln_address::*;
26use login_with_zbd::*;
27use payments::*;
28use rand::Rng;
29use reqwest::{RequestBuilder, Response};
30use serde::{de::DeserializeOwned, Deserialize, Serialize};
31use serde_json::Value;
32use sha2::{Digest, Sha256};
33use utilities::*;
34use validator::Validate;
35use voucher::*;
36use wallet::*;
37use withdrawal_request::*;
38
39pub type Result<T, E = errors::ZebedeeError> = std::result::Result<T, E>;
40
41#[derive(Clone, Debug)]
42pub struct ZebedeeClient {
43 domain: String,
44 reqw_cli: reqwest::Client,
45 apikey: String,
46 oauth: ZebedeeOauth,
47}
48
49impl ZebedeeClient {
50 pub fn new<'a>(apikey: impl Into<Cow<'a, str>>) -> Self {
51 Self {
52 apikey: apikey.into().into_owned(),
53 domain: "https://api.zebedee.io".to_owned(),
54 reqw_cli: reqwest::Client::new(),
55 oauth: Default::default(),
56 }
57 }
58
59 pub fn domain(self, domain: String) -> Self {
61 Self { domain, ..self }
62 }
63
64 pub fn reqw_cli(self, reqw_cli: reqwest::Client) -> Self {
65 Self { reqw_cli, ..self }
66 }
67 pub fn oauth(
68 self,
69 client_id: String,
70 secret: String,
71 redirect_uri: String,
72 state: String,
73 scope: String,
74 ) -> Self {
75 let oauth = ZebedeeOauth::new(client_id, secret, redirect_uri, state, scope);
76 Self { oauth, ..self }
77 }
78
79 async fn parse_response<T>(&self, resp: Response) -> Result<T>
80 where
81 T: DeserializeOwned,
82 {
83 let is_success = resp.status().is_success();
84 let body = resp.json::<Value>().await?;
86
87 match is_success {
89 true => {
90 let body = serde_json::from_value::<T>(body)?;
91 Ok(body)
92 }
93 false => {
94 let err_body: ApiError = serde_json::from_value(body)?;
95 Err(err_body.into())
96 }
97 }
98 }
99
100 fn add_headers(&self, request_builder: RequestBuilder) -> RequestBuilder {
101 request_builder
102 .header("Content-Type", "application/json")
103 .header("apikey", &self.apikey)
104 }
105
106 pub async fn get_wallet_details(&self) -> Result<WalletInfoResponse> {
108 let url = format!("{}/v0/wallet", &self.domain);
109 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
110 self.parse_response(resp).await
111 }
112
113 pub async fn keysend(&self, keysend_payload: &Keysend) -> Result<KeysendResponse> {
115 let url = format!("{}/v0/keysend-payment", &self.domain);
116
117 let resp = self
118 .add_headers(self.reqw_cli.post(&url))
119 .json(keysend_payload)
120 .send()
121 .await?;
122
123 self.parse_response(resp).await
124 }
125
126 pub async fn create_charge(&self, charge: &Charge) -> Result<FetchOneChargeResponse> {
130 let url = format!("{}/v0/charges", &self.domain);
131
132 let resp = self
133 .add_headers(self.reqw_cli.post(&url))
134 .json(&charge)
135 .send()
136 .await?;
137
138 self.parse_response(resp).await
139 }
140
141 pub async fn get_charges(&self) -> Result<FetchChargesResponse> {
142 let url = format!("{}/v0/charges", &self.domain);
143 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
144 self.parse_response(resp).await
145 }
146
147 pub async fn get_charge<T>(&self, charge_id: T) -> Result<FetchOneChargeResponse>
149 where
150 T: AsRef<str>,
151 {
152 let url = format!("{}/v0/charges/{}", &self.domain, charge_id.as_ref());
153 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
154 self.parse_response(resp).await
155 }
156
157 pub async fn pay_gamertag(&self, payment: &GamertagPayment) -> Result<GamertagPayResponse> {
159 payment
160 .validate()
161 .map_err(|e| ErrorMsg::BadGamerTagFormat(e.to_string()))?;
162
163 let url = format!("{}/v0/gamertag/send-payment", &self.domain);
164
165 let resp = self
166 .add_headers(self.reqw_cli.post(&url))
167 .json(payment)
168 .send()
169 .await?;
170
171 self.parse_response(resp).await
172 }
173
174 pub async fn fetch_charge_from_gamertag(
176 &self,
177 payment: &GamertagPayment,
178 ) -> Result<GamertagChargeResponse> {
179 payment
180 .validate()
181 .map_err(|e| ErrorMsg::BadPayloadData(e.to_string()))?;
182
183 let url = format!("{}/v0/gamertag/charges", &self.domain);
184
185 let resp = self
186 .add_headers(self.reqw_cli.post(&url))
187 .json(payment)
188 .send()
189 .await?;
190
191 self.parse_response(resp).await
192 }
193
194 pub async fn get_gamertag_tx<T>(&self, transaction_id: T) -> Result<GamertagTxResponse>
197 where
198 T: AsRef<str>,
199 {
200 let url = format!(
201 "{}/v0/gamertag/transaction/{}",
202 &self.domain,
203 transaction_id.as_ref()
204 );
205
206 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
207 self.parse_response(resp).await
208 }
209
210 pub async fn get_userid_by_gamertag<T>(&self, gamertag: T) -> Result<IdFromGamertagResponse>
212 where
213 T: AsRef<str>,
214 {
215 let url = format!("{}/v0/user-id/gamertag/{}", &self.domain, gamertag.as_ref());
216 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
217 self.parse_response(resp).await
218 }
219
220 pub async fn get_gamertag_by_userid<T>(&self, user_id: T) -> Result<GamertagUserIdResponse>
222 where
223 T: AsRef<str>,
224 {
225 let url = format!("{}/v0/gamertag/user-id/{}", &self.domain, user_id.as_ref());
226 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
227 self.parse_response(resp).await
228 }
229
230 pub async fn internal_transfer(
232 &self,
233 internal_transfer_payload: &InternalTransfer,
234 ) -> Result<InternalTransferResponse> {
235 let url = format!("{}/v0/internal-transfer", &self.domain);
236 let resp = self
237 .add_headers(self.reqw_cli.post(&url))
238 .json(internal_transfer_payload)
239 .send()
240 .await?;
241
242 self.parse_response(resp).await
243 }
244
245 pub async fn pay_ln_address(&self, payment: &LnPayment) -> Result<PayLnAddressResponse> {
247 let url = format!("{}/v0/ln-address/send-payment", &self.domain);
248 let resp = self
249 .add_headers(self.reqw_cli.post(&url))
250 .json(payment)
251 .send()
252 .await?;
253
254 self.parse_response(resp).await
255 }
256
257 pub async fn fetch_charge_ln_address(
259 &self,
260 payment: &LnFetchCharge,
261 ) -> Result<FetchLnChargeResponse> {
262 let url = format!("{}/v0/ln-address/fetch-charge", &self.domain);
263
264 let resp = self
265 .add_headers(self.reqw_cli.post(&url))
266 .json(payment)
267 .send()
268 .await?;
269
270 self.parse_response(resp).await
271 }
272
273 pub async fn validate_ln_address(
275 &self,
276 lightning_address: &LnAddress,
277 ) -> Result<ValidateLnAddrResponse> {
278 lightning_address.validate().map_err(|e| {
279 ErrorMsg::BadLnAddress(lightning_address.address.clone(), e.to_string())
280 })?;
281
282 let url = format!(
283 "{}/v0/ln-address/validate/{}",
284 &self.domain, &lightning_address.address
285 );
286
287 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
288
289 self.parse_response(resp).await
290 }
291
292 pub async fn pay_invoice(&self, payment: &Payment) -> Result<PaymentInvoiceResponse> {
294 let url = format!("{}/v0/payments", &self.domain);
295
296 let resp = self
297 .add_headers(self.reqw_cli.post(&url))
298 .json(&payment)
299 .send()
300 .await?;
301
302 self.parse_response(resp).await
303 }
304
305 pub async fn get_payments(&self) -> Result<FetchPaymentsResponse> {
306 let url = format!("{}/v0/payments", &self.domain);
307 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
308 self.parse_response(resp).await
309 }
310
311 pub async fn get_payment<T>(&self, payment_id: T) -> Result<FetchOnePaymentsResponse>
313 where
314 T: AsRef<str>,
315 {
316 let url = format!("{}/v0/payments/{}", &self.domain, payment_id.as_ref());
317 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
318 self.parse_response(resp).await
319 }
320
321 pub async fn get_is_supported_region_by_ip<T>(&self, ip: T) -> Result<SupportedIpResponse>
323 where
324 T: AsRef<str>,
325 {
326 let url = format!("{}/v0/is-supported-region/{}", &self.domain, ip.as_ref());
327 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
328 self.parse_response(resp).await
329 }
330
331 pub async fn get_prod_ips(&self) -> Result<ProdIpsResponse> {
333 let url = format!("{}/v0/prod-ips", &self.domain);
334 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
335 self.parse_response(resp).await
336 }
337
338 pub async fn get_btc_usd(&self) -> Result<BtcToUsdResponse> {
342 let url = format!("{}/v0/btcusd", &self.domain);
343 let resp = self.reqw_cli.get(&url).send().await?;
344 self.parse_response(resp).await
345 }
346
347 pub async fn create_withdrawal_request(
355 &self,
356 withdrawal_request: &WithdrawalReqest,
357 ) -> Result<CreateWithdrawalResponse> {
358 let url = format!("{}/v0/withdrawal-requests", &self.domain);
359
360 let resp = self
361 .add_headers(self.reqw_cli.post(&url))
362 .json(&withdrawal_request)
363 .send()
364 .await?;
365
366 self.parse_response(resp).await
367 }
368
369 pub async fn get_withdrawal_requests(&self) -> Result<FetchWithdrawalsResponse> {
370 let url = format!("{}/v0/withdrawal-requests", &self.domain);
371 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
372 self.parse_response(resp).await
373 }
374
375 pub async fn get_withdrawal_request<T>(
377 &self,
378 withdrawal_id: T,
379 ) -> Result<FetchOneWithdrawalResponse>
380 where
381 T: AsRef<str>,
382 {
383 let url = format!(
384 "{}/v0/withdrawal-requests/{}",
385 &self.domain,
386 withdrawal_id.as_ref()
387 );
388 let resp = self.add_headers(self.reqw_cli.get(&url)).send().await?;
389 self.parse_response(resp).await
390 }
391
392 pub async fn pay_email(
394 &self,
395 email_payment_request: &EmailPaymentReqest,
396 ) -> Result<EmailPaymentResponse> {
397 let url = format!("{}/v0/email/send-payment", &self.domain);
398
399 let resp = self
400 .add_headers(self.reqw_cli.post(&url))
401 .header("Content-Type", "application/json")
402 .json(&email_payment_request)
403 .send()
404 .await?;
405
406 self.parse_response(resp).await
407 }
408
409 pub async fn create_auth_url<T>(&self, challenge: T) -> Result<String>
410 where
411 T: AsRef<str>,
412 {
413 let url = format!("{}/v1/oauth2/authorize", &self.domain);
414
415 let auth_url = self
416 .reqw_cli
417 .get(url)
418 .header("Content-Type", "application/json")
419 .query(&[("client_id", &self.oauth.client_id)])
420 .query(&[("response_type", "code")])
421 .query(&[("redirect_uri", &self.oauth.redirect_uri)])
422 .query(&[("code_challenge_method", "S256")])
423 .query(&[("code_challenge", challenge.as_ref())])
424 .query(&[("scope", &self.oauth.scope)])
425 .query(&[("state", &self.oauth.state)])
426 .build()
427 .unwrap()
428 .url()
429 .to_string();
430
431 AuthURL::new(&auth_url).validate()?;
432
433 Ok(auth_url)
434 }
435
436 pub async fn fetch_token<A, B>(&self, code: A, verifier: B) -> Result<FetchAccessTokenRes>
437 where
438 A: AsRef<str>,
439 B: AsRef<str>,
440 {
441 let payload = FetchTokenBody::new(self, code.as_ref(), verifier.as_ref());
442 payload.validate()?;
443
444 let url = format!("{}/v1/oauth2/token", &self.domain);
445
446 let resp = self
447 .reqw_cli
448 .post(&url)
449 .header("Content-Type", "application/json")
450 .json(&payload)
451 .send()
452 .await?;
453
454 self.parse_response(resp).await
455 }
456
457 pub async fn refresh_token<T>(&self, refresh_token: T) -> Result<FetchPostRes>
459 where
460 T: AsRef<str>,
461 {
462 let payload = FetchRefresh::new(self, refresh_token.as_ref());
463 payload.validate()?;
464
465 let url = format!("{}/v1/oauth2/token", &self.domain);
466 let resp = self
467 .reqw_cli
468 .post(&url)
469 .header("Content-Type", "application/json")
470 .json(&payload)
471 .send()
472 .await?;
473
474 self.parse_response(resp).await
475 }
476
477 pub async fn fetch_user_data<T>(&self, token: T) -> Result<StdResp<ZBDUserData>>
480 where
481 T: AsRef<str>,
482 {
483 let url = format!("{}/v1/oauth2/user", &self.domain);
487
488 let resp = self
489 .add_headers(self.reqw_cli.get(&url))
490 .header("usertoken", token.as_ref())
491 .send()
492 .await?;
493
494 self.parse_response(resp).await
495 }
496
497 pub async fn fetch_user_wallet_data<T>(&self, token: T) -> Result<StdResp<ZBDUserWalletData>>
499 where
500 T: AsRef<str>,
501 {
502 let url = format!("{}/v1/oauth2/wallet", &self.domain);
506
507 let resp = self
508 .add_headers(self.reqw_cli.get(&url))
509 .header("usertoken", token.as_ref())
510 .send()
511 .await?;
512
513 self.parse_response(resp).await
514 }
515}
516
517#[derive(Default, Clone, Validate, Deserialize, Debug)]
518pub struct ZebedeeOauth {
519 #[validate(length(equal = 36))]
520 client_id: String,
521 #[validate(length(equal = 36))]
522 secret: String,
523 #[validate(url)]
524 redirect_uri: String,
525 #[validate(length(equal = 36))]
526 state: String,
527 scope: String,
528}
529
530impl ZebedeeOauth {
531 fn new(
532 client_id: String,
533 secret: String,
534 redirect_uri: String,
535 state: String,
536 scope: String,
537 ) -> Self {
538 ZebedeeOauth {
539 client_id,
540 secret,
541 redirect_uri,
542 state,
543 scope,
544 }
545 }
546}
547
548#[derive(Clone, Debug, Validate, Deserialize)]
549pub struct PKCE {
550 #[validate(length(equal = 43))]
551 pub verifier: String,
552 #[validate(length(equal = 43))]
553 pub challenge: String,
554}
555
556impl PKCE {
557 pub fn new(input: [u8; 32]) -> Self {
558 let verifier = base64_url::encode(&input);
559
560 let mut hasher_2 = Sha256::new();
561 hasher_2.update(verifier.clone());
562 let hash_result_2 = hasher_2.finalize();
563 let challenge = base64_url::encode(&hash_result_2);
564
565 let p = PKCE {
566 verifier,
567 challenge,
568 };
569 p.validate().unwrap();
570 p
571 }
572
573 pub fn new_rand() -> Self {
574 let random_bytes = rand::thread_rng().gen::<[u8; 32]>();
575 Self::new(random_bytes)
576 }
577}
578
579impl From<&str> for PKCE {
580 fn from(value: &str) -> Self {
581 let mut hasher = Sha256::new();
582 hasher.update(value);
583 let hash_result: [u8; 32] = hasher
584 .finalize()
585 .as_slice()
586 .try_into()
587 .expect("hashing went wrong from string");
588 Self::new(hash_result)
589 }
590}
591
592#[derive(Debug, Serialize, Deserialize)]
593pub struct StdResp<T> {
594 pub success: bool,
595 pub data: T,
596 pub message: Option<String>,
597}