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