Skip to main content

lnurl/
blocking.rs

1//! LNURL by way of `ureq` HTTP client.
2#![allow(clippy::result_large_err)]
3
4use bitcoin::secp256k1::ecdsa::Signature;
5use bitcoin::secp256k1::PublicKey;
6use std::time::Duration;
7
8use ureq::{Agent, Proxy};
9
10use crate::channel::ChannelResponse;
11use crate::lnurl::LnUrl;
12use crate::pay::{LnURLPayInvoice, PayResponse};
13use crate::withdraw::WithdrawalResponse;
14use crate::{decode_ln_url_response_from_json, Builder, Error, LnUrlResponse, Response};
15
16#[derive(Debug, Clone)]
17pub struct BlockingClient {
18    agent: Agent,
19}
20
21impl BlockingClient {
22    /// build a blocking client from a [`Builder`]
23    pub fn from_builder(builder: Builder) -> Result<Self, Error> {
24        let mut agent_builder = ureq::AgentBuilder::new();
25
26        if let Some(timeout) = builder.timeout {
27            agent_builder = agent_builder.timeout(Duration::from_secs(timeout));
28        }
29
30        if let Some(proxy) = &builder.proxy {
31            agent_builder = agent_builder.proxy(Proxy::new(proxy).unwrap());
32        }
33
34        Ok(Self::from_agent(agent_builder.build()))
35    }
36
37    /// build a blocking client from an [`Agent`]
38    pub fn from_agent(agent: Agent) -> Self {
39        BlockingClient { agent }
40    }
41
42    pub fn make_request(&self, url: &str) -> Result<LnUrlResponse, Error> {
43        let resp = self.agent.get(url).call();
44
45        match resp {
46            Ok(resp) => {
47                let json: serde_json::Value = resp.into_json()?;
48                decode_ln_url_response_from_json(json)
49            }
50            Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
51            Err(e) => Err(Error::Ureq(e)),
52        }
53    }
54
55    pub fn get_invoice(
56        &self,
57        pay: &PayResponse,
58        msats: u64,
59        zap_request: Option<String>,
60        comment: Option<&str>,
61    ) -> Result<LnURLPayInvoice, Error> {
62        // verify amount
63        if msats < pay.min_sendable || msats > pay.max_sendable {
64            return Err(Error::InvalidAmount);
65        }
66
67        // verify comment length
68        if let Some(comment) = comment {
69            if let Some(max_length) = pay.comment_allowed {
70                if comment.len() > max_length as usize {
71                    return Err(Error::InvalidComment);
72                }
73            }
74        }
75
76        let symbol = if pay.callback.contains('?') { "&" } else { "?" };
77
78        let url = match (zap_request, comment) {
79            (Some(_), Some(_)) => return Err(Error::InvalidComment),
80            (Some(zap_request), None) => format!(
81                "{}{}amount={}&nostr={}",
82                pay.callback, symbol, msats, zap_request
83            ),
84            (None, Some(comment)) => format!(
85                "{}{}amount={}&comment={}",
86                pay.callback, symbol, msats, comment
87            ),
88            (None, None) => format!("{}{}amount={}", pay.callback, symbol, msats),
89        };
90
91        let resp = self.agent.get(&url).call();
92
93        match resp {
94            Ok(resp) => {
95                let json: serde_json::Value = resp.into_json()?;
96                let result = serde_json::from_value::<LnURLPayInvoice>(json.clone());
97
98                match result {
99                    Ok(invoice) => {
100                        // verify the returned invoice's amount matches the requested amount (LUD-06)
101                        invoice.verify_amount(msats)?;
102                        Ok(invoice)
103                    }
104                    Err(_) => {
105                        let response = serde_json::from_value::<Response<()>>(json)?;
106                        match response {
107                            Response::Error { reason } => Err(Error::Other(reason)),
108                            Response::Ok { .. } => unreachable!("Ok response should be an invoice"),
109                        }
110                    }
111                }
112            }
113            Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
114            Err(e) => Err(Error::Ureq(e)),
115        }
116    }
117
118    pub 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
134        let resp = self.agent.get(&url).call();
135
136        match resp {
137            Ok(resp) => Ok(resp.into_json()?),
138            Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
139            Err(e) => Err(Error::Ureq(e)),
140        }
141    }
142
143    pub fn open_channel(
144        &self,
145        channel: &ChannelResponse,
146        node_pubkey: PublicKey,
147        private: bool,
148    ) -> Result<Response<()>, Error> {
149        let symbol = if channel.callback.contains('?') {
150            "&"
151        } else {
152            "?"
153        };
154
155        let url = format!(
156            "{}{}k1={}&remoteid={}&private={}",
157            channel.callback,
158            symbol,
159            channel.k1,
160            node_pubkey,
161            private as i32 // 0 or 1
162        );
163
164        let resp = self.agent.get(&url).call();
165
166        match resp {
167            Ok(resp) => Ok(resp.into_json()?),
168            Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
169            Err(e) => Err(Error::Ureq(e)),
170        }
171    }
172
173    pub fn lnurl_auth(
174        &self,
175        lnurl: LnUrl,
176        sig: Signature,
177        key: PublicKey,
178    ) -> Result<Response<()>, Error> {
179        let url = format!("{}&sig={}&key={}", lnurl.url, sig, key);
180
181        let resp = self.agent.get(&url).call();
182
183        match resp {
184            Ok(resp) => Ok(resp.into_json()?),
185            Err(ureq::Error::Status(code, _)) => Err(Error::HttpResponse(code)),
186            Err(e) => Err(Error::Ureq(e)),
187        }
188    }
189}