Skip to main content

breez_sdk_spark/sdk/payments/
mod.rs

1use spark_wallet::LightningReceivePayment;
2use tracing::instrument;
3
4use crate::{
5    ClaimHtlcPaymentRequest, ClaimHtlcPaymentResponse, FetchConversionLimitsRequest,
6    FetchConversionLimitsResponse, GetPaymentRequest, GetPaymentResponse, WaitForPaymentIdentifier,
7    error::SdkError,
8    models::{
9        BuildUnsignedTransferPackageRequest, ListPaymentsRequest, ListPaymentsResponse, Payment,
10        PaymentRequest, PrepareSendPaymentRequest, PrepareSendPaymentResponse,
11        PublishSignedTransferPackageRequest, PublishSignedTransferPackageResponse,
12        ReceivePaymentRequest, ReceivePaymentResponse, SendPaymentRequest, SendPaymentResponse,
13        UnsignedTransferPackage,
14    },
15    utils::payments::get_payment_with_conversion_details,
16};
17
18use super::BreezSdk;
19
20pub(in crate::sdk) mod client_signing;
21pub(in crate::sdk) mod conversion;
22mod polling;
23pub(in crate::sdk) mod prepare;
24mod receive;
25pub(in crate::sdk) mod send;
26pub(in crate::sdk) mod validation;
27
28#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
29#[allow(clippy::needless_pass_by_value)]
30impl BreezSdk {
31    pub async fn receive_payment(
32        &self,
33        request: ReceivePaymentRequest,
34    ) -> Result<ReceivePaymentResponse, SdkError> {
35        receive::receive_payment(self, request).await
36    }
37
38    pub async fn claim_htlc_payment(
39        &self,
40        request: ClaimHtlcPaymentRequest,
41    ) -> Result<ClaimHtlcPaymentResponse, SdkError> {
42        receive::claim_htlc_payment(self, request).await
43    }
44
45    pub async fn prepare_send_payment(
46        &self,
47        request: PrepareSendPaymentRequest,
48    ) -> Result<PrepareSendPaymentResponse, SdkError> {
49        // Cross-chain has its own request type (no parse step required) — early-dispatch
50        // before falling through to the generic `Input` path.
51        if let PaymentRequest::CrossChain {
52            ref address,
53            ref route,
54            max_slippage_bps,
55            target_overpay_bps,
56        } = request.payment_request
57        {
58            let amount = request.amount.ok_or(SdkError::InvalidInput(
59                "Amount is required for cross-chain sends".to_string(),
60            ))?;
61            return prepare::cross_chain::prepare(
62                self,
63                address,
64                route,
65                amount,
66                request.token_identifier.clone(),
67                request.conversion_options.clone(),
68                request.fee_policy.unwrap_or_default(),
69                max_slippage_bps,
70                target_overpay_bps,
71            )
72            .await;
73        }
74        prepare::prepare(self, request).await
75    }
76
77    #[instrument(
78        level = "info",
79        target = "breez_sdk_core::send_payment",
80        skip_all,
81        fields(payment_id = tracing::field::Empty),
82    )]
83    pub async fn send_payment(
84        &self,
85        request: SendPaymentRequest,
86    ) -> Result<SendPaymentResponse, SdkError> {
87        self.maybe_ensure_spark_private_mode_initialized().await?;
88        if let Some(key) = request.idempotency_key.as_deref() {
89            tracing::Span::current().record("payment_id", key);
90        }
91        Box::pin(send::orchestrate_send(self, request, false, None)).await
92    }
93
94    pub async fn build_unsigned_transfer_package(
95        &self,
96        request: BuildUnsignedTransferPackageRequest,
97    ) -> Result<UnsignedTransferPackage, SdkError> {
98        Box::pin(client_signing::build_unsigned_transfer_package(
99            self,
100            &request.prepare_response,
101            request.options.as_ref(),
102        ))
103        .await
104    }
105
106    #[instrument(
107        level = "info",
108        target = "breez_sdk_core::publish_signed_transfer_package",
109        skip_all
110    )]
111    pub async fn publish_signed_transfer_package(
112        &self,
113        request: PublishSignedTransferPackageRequest,
114    ) -> Result<PublishSignedTransferPackageResponse, SdkError> {
115        self.maybe_ensure_spark_private_mode_initialized().await?;
116        Box::pin(send::publish_signed_transfer_package(
117            self,
118            &request.signed_package,
119        ))
120        .await
121    }
122
123    pub async fn fetch_conversion_limits(
124        &self,
125        request: FetchConversionLimitsRequest,
126    ) -> Result<FetchConversionLimitsResponse, SdkError> {
127        self.token_converter
128            .fetch_limits(&request)
129            .await
130            .map_err(Into::into)
131    }
132
133    /// Runs one pass of the pending-conversion refunder.
134    ///
135    /// Iterates over payments whose conversions failed and have a refund
136    /// pending, then attempts to refund each one. This is the same logic the
137    /// SDK runs internally on a periodic schedule when
138    /// `background_tasks_enabled` is `true`. When background tasks are
139    /// disabled the periodic refunder does not run, and this method is the
140    /// explicit entry point for driving the pass; when background tasks are
141    /// enabled, it can be called to force an immediate refund pass.
142    pub async fn refund_pending_conversions(&self) -> Result<(), SdkError> {
143        self.token_converter
144            .refund_pending()
145            .await
146            .map_err(Into::into)
147    }
148
149    /// Lists payments from the storage with pagination
150    ///
151    /// This method provides direct access to the payment history stored in the database.
152    /// It returns payments in reverse chronological order (newest first).
153    ///
154    /// # Arguments
155    ///
156    /// * `request` - Contains pagination parameters (offset and limit)
157    ///
158    /// # Returns
159    ///
160    /// * `Ok(ListPaymentsResponse)` - Contains the list of payments if successful
161    /// * `Err(SdkError)` - If there was an error accessing the storage
162    pub async fn list_payments(
163        &self,
164        request: ListPaymentsRequest,
165    ) -> Result<ListPaymentsResponse, SdkError> {
166        use crate::utils::conversions::extract_conversion_info;
167        use crate::utils::payments::build_conversions;
168
169        let mut payments = self.storage.list_payments(request.into()).await?;
170
171        // Query child payments for payments that have conversion_details set (AMM)
172        let parent_ids: Vec<String> = payments
173            .iter()
174            .filter(|p| p.conversion_details.is_some())
175            .map(|p| p.id.clone())
176            .collect();
177
178        let related_payments_map = if parent_ids.is_empty() {
179            std::collections::HashMap::default()
180        } else {
181            self.storage.get_payments_by_parent_ids(parent_ids).await?
182        };
183
184        for payment in &mut payments {
185            let has_conversion_details = payment.conversion_details.is_some();
186            let has_crosschain_info = extract_conversion_info(payment.details.clone())
187                .is_some_and(|info| !matches!(info, crate::ConversionInfo::Amm { .. }));
188
189            if !has_conversion_details && !has_crosschain_info {
190                continue;
191            }
192
193            let child_payments = if has_conversion_details {
194                related_payments_map.get(&payment.id).map(Vec::as_slice)
195            } else {
196                None
197            };
198
199            let conversions = build_conversions(payment, child_payments);
200
201            if !conversions.is_empty() {
202                if let Some(ref mut cd) = payment.conversion_details {
203                    cd.conversions = conversions;
204                } else {
205                    let status = extract_conversion_info(payment.details.clone())
206                        .map_or(crate::ConversionStatus::Completed, |info| {
207                            info.status().clone()
208                        });
209                    payment.conversion_details = Some(crate::models::ConversionDetails {
210                        status,
211                        conversions,
212                    });
213                }
214            }
215        }
216
217        Ok(ListPaymentsResponse { payments })
218    }
219
220    pub async fn get_payment(
221        &self,
222        request: GetPaymentRequest,
223    ) -> Result<GetPaymentResponse, SdkError> {
224        let payment =
225            get_payment_with_conversion_details(request.payment_id, self.storage.clone()).await?;
226
227        Ok(GetPaymentResponse { payment })
228    }
229}
230
231// Private payment methods
232impl BreezSdk {
233    pub(crate) async fn receive_bolt11_invoice(
234        &self,
235        description: String,
236        amount_sats: Option<u64>,
237        expiry_secs: Option<u32>,
238        payment_hash: Option<String>,
239    ) -> Result<ReceivePaymentResponse, SdkError> {
240        receive::receive_bolt11_invoice(self, description, amount_sats, expiry_secs, payment_hash)
241            .await
242    }
243
244    pub(crate) async fn receive_bolt11_invoice_inner(
245        &self,
246        description: String,
247        amount_sats: Option<u64>,
248        expiry_secs: Option<u32>,
249        payment_hash: Option<String>,
250    ) -> Result<LightningReceivePayment, SdkError> {
251        receive::receive_bolt11_invoice_inner(
252            self,
253            description,
254            amount_sats,
255            expiry_secs,
256            payment_hash,
257        )
258        .await
259    }
260
261    pub(crate) async fn wait_for_incoming_payment(
262        &self,
263        identifier: WaitForPaymentIdentifier,
264        completion_timeout_secs: u32,
265    ) -> Result<Payment, SdkError> {
266        polling::wait_for_incoming_payment(self, identifier, completion_timeout_secs).await
267    }
268
269    pub(crate) async fn finalize_payment(&self, payment: Payment) -> bool {
270        polling::finalize_payment(self, payment).await
271    }
272}