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        // Generate a Lightning invoice for the withdraw, keeping the SSP-side
91        // receive id for the targeted wait below.
92        let receive = self
93            .receive_bolt11_invoice_inner(
94                withdraw_request.default_description.clone(),
95                Some(amount_sats),
96                None,
97                None,
98            )
99            .await?;
100        let payment_request = receive.invoice.clone();
101        let ssp_receive_id = receive.id;
102
103        // Store the LNURL withdraw metadata before executing the withdraw
104        let cache = ObjectCacheRepository::new(self.storage.clone());
105        cache
106            .save_payment_metadata(
107                &payment_request,
108                &PaymentMetadata {
109                    lnurl_withdraw_info: Some(LnurlWithdrawInfo {
110                        withdraw_url: withdraw_request.callback.clone(),
111                    }),
112                    lnurl_description: Some(withdraw_request.default_description.clone()),
113                    ..Default::default()
114                },
115            )
116            .await?;
117
118        // Perform the LNURL withdraw using the generated invoice
119        let withdraw_response = execute_lnurl_withdraw(
120            self.lnurl_client.as_ref(),
121            &withdraw_request,
122            &payment_request,
123        )
124        .await?;
125        if let lnurl::withdraw::ValidatedCallbackResponse::EndpointError { data } =
126            withdraw_response
127        {
128            return Err(LnurlError::EndpointError(data.reason).into());
129        }
130
131        let completion_timeout_secs = match completion_timeout_secs {
132            Some(secs) if secs > 0 => secs,
133            _ => {
134                return Ok(LnurlWithdrawResponse {
135                    payment_request,
136                    payment: None,
137                });
138            }
139        };
140
141        // Wait for the LNURL service to pay the invoice
142        let payment = self
143            .wait_for_incoming_payment(
144                WaitForPaymentIdentifier::LightningReceive {
145                    invoice: payment_request.clone(),
146                    ssp_id: ssp_receive_id,
147                },
148                completion_timeout_secs,
149            )
150            .await
151            .ok();
152        Ok(LnurlWithdrawResponse {
153            payment_request,
154            payment,
155        })
156    }
157
158    /// Performs LNURL-auth with the service.
159    ///
160    /// This method implements the LNURL-auth protocol as specified in LUD-04 and LUD-05.
161    /// It derives a domain-specific linking key, signs the challenge, and sends the
162    /// authentication request to the service.
163    pub async fn lnurl_auth(
164        &self,
165        request_data: LnurlAuthRequestDetails,
166    ) -> Result<LnurlCallbackStatus, SdkError> {
167        // LNURL-auth needs the HMAC step, absent on a signing-only signer.
168        let Some(lnurl_auth_signer) = self.lnurl_auth_signer.as_ref() else {
169            return Err(SdkError::Generic(
170                "LNURL-auth requires a signer that supports HMAC".to_string(),
171            ));
172        };
173        let request: breez_sdk_common::lnurl::auth::LnurlAuthRequestDetails = request_data.into();
174        let status = breez_sdk_common::lnurl::auth::perform_lnurl_auth(
175            self.lnurl_client.as_ref(),
176            &request,
177            lnurl_auth_signer.as_ref(),
178        )
179        .await
180        .map_err(|e| match e {
181            LnurlError::ServiceConnectivity(msg) => SdkError::NetworkError(msg.to_string()),
182            LnurlError::InvalidUri(msg) => SdkError::InvalidInput(msg),
183            _ => SdkError::Generic(e.to_string()),
184        })?;
185        Ok(status.into())
186    }
187}