zebedee_rust/
lib.rs

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    /// Zebedee REST API url
60    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        // parse the resp body
85        let body = resp.json::<Value>().await?;
86
87        // based on success or error choose the appropriate data structure to deserialize
88        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    /// Retrieves the total balance of a given Project Wallet.
107    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    /// Make payment directly to a Lightning Network node Public Key, without the need for a Payment Request / Charge.
114    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    /// Creates a new Charge / Payment Request in the Bitcoin Lightning Network, payable by any Lightning Network wallet.
127    /// These payment requests are single-use, fixed-amount QR codes. If you're looking for multi-use and multi-amount
128    /// payment requests you want Static Charges.
129    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    /// Retrieves all information relating a specific Charge / Payment Request.
148    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    /// Send Bitcoin payments directly to a user's ZBD Gamertag
158    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    /// Create a bolt 11 invoice so you can pay a specified gamertag
175    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    /// Get data on payments sent to ZBD Gamertags.
195    /// The data payload returned will inform you of the status of that transaction as well as any associated fees.
196    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    /// Get a given User's ID when provided with a ZBD Gamertag.
211    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    /// Get a given user's ZBD Gamertag from user id
221    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    /// Initiates a transfer of funds between two Project Wallets you own.
231    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    /// Send Bitcoin payments directly to a Lightning Address.
246    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    /// Create a Charge / Payment Request QR code for a Lightning Address
258    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    /// Validate whether a user's entered Lightning Address is indeed a real Lightning Address
274    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    /// Pays a Charge / Payment Request in the Bitcoin Lightning Network
293    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    /// Retrieves all the information related to a specific Payment
312    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    /// Check if provided ip address will be [supported](https://zebedee.io/countries) by Zebedee REST API
322    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    /// Check if callback response is from legit Zebedee ip address
332    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    /// Get the latest price for Bitcoin in US Dollars.
339    /// The exchange rate feed is refreshed every 5 seconds and is based upon a combination of industry-leading
340    /// partner exchange providers's price feeds.
341    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    /// Withdrawal Requests can be thought of as exact opposites to Charges.
348    /// Charges in the ZEBEDEE API are QR codes that represent Payment Requests in the Bitcoin Lightning Network.
349    /// These QR codes expect that a payer will scan and perform a payment against it.
350    /// ***
351    /// `Charges`: Lightning QR codes that YOU SPEND
352    /// ***
353    /// `Withdrawal Requests`: Lightning QR codes that YOU RECEIVE
354    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    /// Retrieves details about a specific Withdrawal Request.
376    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    /// Send instant Bitcoin payments to any email.
393    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    /// In order to fetch a new accessToken for a given ZBD User, make sure to use the refreshToken using the token endpoint.
458    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    /// You can use this API endpoint to fetch information about a given ZBD User, granted you can pass the provided accessToken.
478
479    pub async fn fetch_user_data<T>(&self, token: T) -> Result<StdResp<ZBDUserData>>
480    where
481        T: AsRef<str>,
482    {
483        //let mut token_header_string: String = "Bearer ".to_owned();
484        //token_header_string.push_str(&bearer_token);
485
486        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    /// You can use this API endpoint to fetch information about a given ZBD User's Wallet, granted you can pass the provided accessToken.
498    pub async fn fetch_user_wallet_data<T>(&self, token: T) -> Result<StdResp<ZBDUserWalletData>>
499    where
500        T: AsRef<str>,
501    {
502        //let mut token_header_string: String = "Bearer ".to_owned();
503        //token_header_string.push_str(&bearer_token);
504
505        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}