Skip to main content

lnurl/
async.rs

1//! LNURL by way of `reqwest` HTTP client.
2#![allow(clippy::result_large_err)]
3
4use bitcoin::secp256k1::ecdsa::Signature;
5use bitcoin::secp256k1::PublicKey;
6use reqwest::Client;
7
8use crate::api::*;
9use crate::channel::ChannelResponse;
10use crate::lnurl::LnUrl;
11use crate::pay::{LnURLPayInvoice, PayResponse, VerifyResponse};
12use crate::withdraw::WithdrawalResponse;
13use crate::{Builder, Error};
14
15#[derive(Debug, Clone)]
16pub struct AsyncClient {
17    pub client: Client,
18}
19
20impl Default for AsyncClient {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl AsyncClient {
27    pub fn new() -> Self {
28        Self {
29            client: Client::new(),
30        }
31    }
32
33    /// build an async client from a builder
34    pub fn from_builder(builder: Builder) -> Result<Self, Error> {
35        let mut client_builder = Client::builder();
36
37        #[cfg(not(target_arch = "wasm32"))]
38        if let Some(proxy) = &builder.proxy {
39            client_builder = client_builder.proxy(reqwest::Proxy::all(proxy)?);
40        }
41
42        #[cfg(not(target_arch = "wasm32"))]
43        if let Some(timeout) = builder.timeout {
44            client_builder = client_builder.timeout(core::time::Duration::from_secs(timeout));
45        }
46
47        Ok(Self::from_client(client_builder.build()?))
48    }
49
50    /// build an async client from the base url and [`Client`]
51    pub fn from_client(client: Client) -> Self {
52        AsyncClient { client }
53    }
54
55    pub async fn make_request(&self, url: &str) -> Result<LnUrlResponse, Error> {
56        let resp = self.client.get(url).send().await?;
57
58        let txt = resp.error_for_status()?.text().await?;
59        decode_ln_url_response(&txt)
60    }
61
62    pub async fn get_invoice(
63        &self,
64        pay: &PayResponse,
65        msats: u64,
66        zap_request: Option<String>,
67        comment: Option<&str>,
68    ) -> Result<LnURLPayInvoice, Error> {
69        // verify amount
70        if msats < pay.min_sendable || msats > pay.max_sendable {
71            return Err(Error::InvalidAmount);
72        }
73
74        // verify comment length
75        if let Some(comment) = comment {
76            if let Some(max_length) = pay.comment_allowed {
77                if comment.len() > max_length as usize {
78                    return Err(Error::InvalidComment);
79                }
80            }
81        }
82
83        let symbol = if pay.callback.contains('?') { "&" } else { "?" };
84
85        let url = match (zap_request, comment) {
86            (Some(_), Some(_)) => return Err(Error::InvalidComment),
87            (Some(zap_request), None) => format!(
88                "{}{}amount={}&nostr={}",
89                pay.callback, symbol, msats, zap_request
90            ),
91            (None, Some(comment)) => format!(
92                "{}{}amount={}&comment={}",
93                pay.callback, symbol, msats, comment
94            ),
95            (None, None) => format!("{}{}amount={}", pay.callback, symbol, msats),
96        };
97
98        let resp = self.client.get(&url).send().await?;
99
100        let invoice: LnURLPayInvoice = resp.error_for_status()?.json().await?;
101
102        // verify the returned invoice's amount matches the requested amount (LUD-06)
103        invoice.verify_amount(msats)?;
104
105        Ok(invoice)
106    }
107
108    pub async fn verify(&self, url: &str) -> Result<VerifyResponse, Error> {
109        let resp = self.client.get(url).send().await?;
110
111        let rsp: Response<VerifyResponse> = resp.error_for_status()?.json().await?;
112        match rsp {
113            Response::Error { reason } => Err(Error::Other(reason)),
114            Response::Ok(r) => Ok(r),
115        }
116    }
117
118    pub async fn do_withdrawal(
119        &self,
120        withdrawal: &WithdrawalResponse,
121        invoice: &str,
122    ) -> Result<Response<()>, Error> {
123        let symbol = if withdrawal.callback.contains('?') {
124            "&"
125        } else {
126            "?"
127        };
128
129        let url = format!(
130            "{}{}k1={}&pr={}",
131            withdrawal.callback, symbol, withdrawal.k1, invoice
132        );
133        let resp = self.client.get(url).send().await?;
134
135        Ok(resp.error_for_status()?.json().await?)
136    }
137
138    pub async fn open_channel(
139        &self,
140        channel: &ChannelResponse,
141        node_pubkey: PublicKey,
142        private: bool,
143    ) -> Result<Response<()>, Error> {
144        let symbol = if channel.callback.contains('?') {
145            "&"
146        } else {
147            "?"
148        };
149
150        let url = format!(
151            "{}{}k1={}&remoteid={}&private={}",
152            channel.callback,
153            symbol,
154            channel.k1,
155            node_pubkey,
156            private as i32 // 0 or 1
157        );
158
159        let resp = self.client.get(url).send().await?;
160
161        Ok(resp.error_for_status()?.json().await?)
162    }
163
164    pub async fn lnurl_auth(
165        &self,
166        lnurl: LnUrl,
167        sig: Signature,
168        key: PublicKey,
169    ) -> Result<Response<()>, Error> {
170        let url = format!("{}&sig={}&key={}", lnurl.url, sig, key);
171
172        let resp = self.client.get(url).send().await?;
173
174        Ok(resp.error_for_status()?.json().await?)
175    }
176}