Skip to main content

breez_sdk_spark/sdk/lnurl/
mod.rs

1use breez_sdk_common::lnurl::{self, error::LnurlError};
2
3use crate::{
4    BuildUnsignedLnurlPayPackageRequest, LnurlAuthRequestDetails, LnurlCallbackStatus,
5    LnurlPayRequest, LnurlPayResponse, LnurlWithdrawInfo, LnurlWithdrawRequest,
6    LnurlWithdrawResponse, PrepareLnurlPayRequest, PrepareLnurlPayResponse,
7    PublishSignedLnurlPayPackageRequest, PublishSignedLnurlPayResponse, UnsignedTransferPackage,
8    WaitForPaymentIdentifier,
9    error::SdkError,
10    persist::{ObjectCacheRepository, PaymentMetadata},
11};
12use breez_sdk_common::lnurl::withdraw::execute_lnurl_withdraw;
13
14use super::BreezSdk;
15
16mod pay;
17
18#[macros::instance_span]
19#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
20#[allow(clippy::needless_pass_by_value)]
21impl BreezSdk {
22    pub async fn prepare_lnurl_pay(
23        &self,
24        request: PrepareLnurlPayRequest,
25    ) -> Result<PrepareLnurlPayResponse, SdkError> {
26        pay::prepare(self, request).await
27    }
28
29    pub async fn lnurl_pay(&self, request: LnurlPayRequest) -> Result<LnurlPayResponse, SdkError> {
30        pay::send(self, request).await
31    }
32
33    pub async fn build_unsigned_lnurl_pay_package(
34        &self,
35        request: BuildUnsignedLnurlPayPackageRequest,
36    ) -> Result<UnsignedTransferPackage, SdkError> {
37        pay::build_package(self, &request.prepare_response).await
38    }
39
40    pub async fn publish_signed_lnurl_pay_package(
41        &self,
42        request: PublishSignedLnurlPayPackageRequest,
43    ) -> Result<PublishSignedLnurlPayResponse, SdkError> {
44        self.maybe_ensure_spark_private_mode_initialized().await?;
45        pay::publish_signed_package(self, request.signed_package).await
46    }
47
48    /// Performs an LNURL withdraw operation for the amount of satoshis to
49    /// withdraw and the LNURL withdraw request details. The LNURL withdraw request
50    /// details can be obtained from calling [`BreezSdk::parse`].
51    ///
52    /// The method generates a Lightning invoice for the withdraw amount, stores
53    /// the LNURL withdraw metadata, and performs the LNURL withdraw using  the generated
54    /// invoice.
55    ///
56    /// If the `completion_timeout_secs` parameter is provided and greater than 0, the
57    /// method will wait for the payment to be completed within that period. If the
58    /// withdraw is completed within the timeout, the `payment` field in the response
59    /// will be set with the payment details. If the `completion_timeout_secs`
60    /// parameter is not provided or set to 0, the method will not wait for the payment
61    /// to be completed. If the withdraw is not completed within the
62    /// timeout, the `payment` field will be empty.
63    ///
64    /// # Arguments
65    ///
66    /// * `request` - The LNURL withdraw request
67    ///
68    /// # Returns
69    ///
70    /// Result containing either:
71    /// * `LnurlWithdrawResponse` - The payment details if the withdraw request was successful
72    /// * `SdkError` - If there was an error during the withdraw process
73    pub async fn lnurl_withdraw(
74        &self,
75        request: LnurlWithdrawRequest,
76    ) -> Result<LnurlWithdrawResponse, SdkError> {
77        self.maybe_ensure_spark_private_mode_initialized().await?;
78        let LnurlWithdrawRequest {
79            amount_sats,
80            withdraw_request,
81            completion_timeout_secs,
82        } = request;
83        let withdraw_request: breez_sdk_common::lnurl::withdraw::LnurlWithdrawRequestDetails =
84            withdraw_request.into();
85        if !withdraw_request.is_amount_valid(amount_sats) {
86            return Err(SdkError::InvalidInput(
87                "Amount must be within min/max LNURL withdrawable limits".to_string(),
88            ));
89        }
90
91        // Reject an unsafe callback now, before an invoice is created and
92        // metadata stored for a withdraw that cannot proceed. The execute
93        // step revalidates (and adds the DNS preflight).
94        lnurl::security::validate_callback_url(
95            &withdraw_request.callback,
96            lnurl::security::callback_trust(&withdraw_request.url),
97        )?;
98
99        // Generate a Lightning invoice for the withdraw, keeping the SSP-side
100        // receive id for the targeted wait below.
101        let receive = self
102            .receive_bolt11_invoice_inner(
103                withdraw_request.default_description.clone(),
104                Some(amount_sats),
105                None,
106                None,
107                None,
108                None,
109            )
110            .await?;
111        let payment_request = receive.invoice.clone();
112        let ssp_receive_id = receive.id;
113
114        // Store the LNURL withdraw metadata before executing the withdraw
115        let cache = ObjectCacheRepository::new(self.storage.clone());
116        cache
117            .save_payment_metadata(
118                &payment_request,
119                &PaymentMetadata {
120                    lnurl_withdraw_info: Some(LnurlWithdrawInfo {
121                        withdraw_url: withdraw_request.callback.clone(),
122                    }),
123                    lnurl_description: Some(withdraw_request.default_description.clone()),
124                    ..Default::default()
125                },
126            )
127            .await?;
128
129        // Perform the LNURL withdraw using the generated invoice
130        let withdraw_response = execute_lnurl_withdraw(
131            self.lnurl_client.as_ref(),
132            &withdraw_request,
133            &payment_request,
134            // DNS preflight of the callback host runs only when unproxied:
135            // with a proxy the lookup would run outside it and leak hostnames.
136            self.config.proxy.is_none(),
137        )
138        .await?;
139        if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
140            withdraw_response
141        {
142            return Err(LnurlError::EndpointError(data.reason).into());
143        }
144
145        let completion_timeout_secs = match completion_timeout_secs {
146            Some(secs) if secs > 0 => secs,
147            _ => {
148                return Ok(LnurlWithdrawResponse {
149                    payment_request,
150                    payment: None,
151                });
152            }
153        };
154
155        // Wait for the LNURL service to pay the invoice
156        let payment = self
157            .wait_for_incoming_payment(
158                WaitForPaymentIdentifier::LightningReceive {
159                    invoice: payment_request.clone(),
160                    ssp_id: ssp_receive_id,
161                },
162                completion_timeout_secs,
163            )
164            .await
165            .ok();
166        Ok(LnurlWithdrawResponse {
167            payment_request,
168            payment,
169        })
170    }
171
172    /// Performs LNURL-auth with the service.
173    ///
174    /// This method implements the LNURL-auth protocol as specified in LUD-04 and LUD-05.
175    /// It derives a domain-specific linking key, signs the challenge, and sends the
176    /// authentication request to the service.
177    pub async fn lnurl_auth(
178        &self,
179        request_data: LnurlAuthRequestDetails,
180    ) -> Result<LnurlCallbackStatus, SdkError> {
181        // LNURL-auth needs the HMAC step, absent on a signing-only signer.
182        let Some(lnurl_auth_signer) = self.lnurl_auth_signer.as_ref() else {
183            return Err(SdkError::Generic(
184                "LNURL-auth requires a signer that supports HMAC".to_string(),
185            ));
186        };
187        let request: breez_sdk_common::lnurl::auth::LnurlAuthRequestDetails = request_data.into();
188        let status = breez_sdk_common::lnurl::auth::perform_lnurl_auth(
189            self.lnurl_client.as_ref(),
190            &request,
191            lnurl_auth_signer.as_ref(),
192        )
193        .await
194        .map_err(|e| match e {
195            LnurlError::ServiceConnectivity(msg) => SdkError::NetworkError(msg.to_string()),
196            LnurlError::InvalidUri(msg) => SdkError::InvalidInput(msg),
197            _ => SdkError::Generic(e.to_string()),
198        })?;
199        Ok(status.into())
200    }
201}