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