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