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