breez_sdk_spark/models/
adaptors.rs

1use breez_sdk_common::input::{
2    self, InputType, PaymentRequestSource, SparkInvoiceDetails, parse_spark_address,
3};
4use spark_wallet::{
5    CoopExitFeeQuote, CoopExitSpeedFeeQuote, ExitSpeed, LightningSendPayment, LightningSendStatus,
6    Network as SparkNetwork, PreimageRequest, PreimageRequestStatus, SspUserRequest,
7    TokenTransactionStatus, TransferDirection, TransferStatus, TransferType, WalletTransfer,
8};
9use tracing::debug;
10use web_time::UNIX_EPOCH;
11
12use crate::{
13    Fee, Network, OnchainConfirmationSpeed, OptimizationProgress, Payment, PaymentDetails,
14    PaymentMethod, PaymentStatus, PaymentType, SdkError, SendOnchainFeeQuote,
15    SendOnchainSpeedFeeQuote, SparkHtlcDetails, SparkHtlcStatus, SparkInvoicePaymentDetails,
16    TokenBalance, TokenMetadata,
17};
18
19impl PaymentMethod {
20    fn from_transfer(transfer: &WalletTransfer) -> Self {
21        match transfer.transfer_type {
22            TransferType::PreimageSwap => {
23                if transfer.is_ssp_transfer {
24                    PaymentMethod::Lightning
25                } else {
26                    PaymentMethod::Spark
27                }
28            }
29            TransferType::CooperativeExit => PaymentMethod::Withdraw,
30            TransferType::UtxoSwap => PaymentMethod::Deposit,
31            TransferType::Transfer => PaymentMethod::Spark,
32            _ => PaymentMethod::Unknown,
33        }
34    }
35}
36
37impl PaymentDetails {
38    fn from_transfer(transfer: &WalletTransfer) -> Result<Option<Self>, SdkError> {
39        if !transfer.is_ssp_transfer {
40            // Check for Spark invoice payments
41            if let Some(spark_invoice) = &transfer.spark_invoice {
42                let Some(InputType::SparkInvoice(invoice_details)) =
43                    parse_spark_address(spark_invoice, &PaymentRequestSource::default())
44                else {
45                    return Err(SdkError::Generic("Invalid spark invoice".to_string()));
46                };
47
48                return Ok(Some(PaymentDetails::Spark {
49                    invoice_details: Some(invoice_details.into()),
50                    htlc_details: None,
51                    conversion_info: None,
52                }));
53            }
54
55            // Check for Spark HTLC payments (when no user request is present)
56            if let Some(htlc_preimage_request) = &transfer.htlc_preimage_request {
57                return Ok(Some(PaymentDetails::Spark {
58                    invoice_details: None,
59                    htlc_details: Some(htlc_preimage_request.clone().try_into()?),
60                    conversion_info: None,
61                }));
62            }
63
64            return Ok(Some(PaymentDetails::Spark {
65                invoice_details: None,
66                htlc_details: None,
67                conversion_info: None,
68            }));
69        }
70
71        let Some(user_request) = &transfer.user_request else {
72            return Ok(None);
73        };
74
75        let details = match user_request {
76            SspUserRequest::LightningReceiveRequest(request) => {
77                let invoice_details = input::parse_invoice(&request.invoice.encoded_invoice)
78                    .ok_or(SdkError::Generic(
79                        "Invalid invoice in SspUserRequest::LightningReceiveRequest".to_string(),
80                    ))?;
81                PaymentDetails::Lightning {
82                    description: invoice_details.description,
83                    preimage: request.lightning_receive_payment_preimage.clone(),
84                    invoice: request.invoice.encoded_invoice.clone(),
85                    payment_hash: request.invoice.payment_hash.clone(),
86                    destination_pubkey: invoice_details.payee_pubkey,
87                    lnurl_pay_info: None,
88                    lnurl_withdraw_info: None,
89                    lnurl_receive_metadata: None,
90                }
91            }
92            SspUserRequest::LightningSendRequest(request) => {
93                let invoice_details =
94                    input::parse_invoice(&request.encoded_invoice).ok_or(SdkError::Generic(
95                        "Invalid invoice in SspUserRequest::LightningSendRequest".to_string(),
96                    ))?;
97                PaymentDetails::Lightning {
98                    description: invoice_details.description,
99                    preimage: request.lightning_send_payment_preimage.clone(),
100                    invoice: request.encoded_invoice.clone(),
101                    payment_hash: invoice_details.payment_hash,
102                    destination_pubkey: invoice_details.payee_pubkey,
103                    lnurl_pay_info: None,
104                    lnurl_withdraw_info: None,
105                    lnurl_receive_metadata: None,
106                }
107            }
108            SspUserRequest::CoopExitRequest(request) => PaymentDetails::Withdraw {
109                tx_id: request.coop_exit_txid.clone(),
110            },
111            SspUserRequest::LeavesSwapRequest(_) => PaymentDetails::Spark {
112                invoice_details: None,
113                htlc_details: None,
114                conversion_info: None,
115            },
116            SspUserRequest::ClaimStaticDeposit(request) => PaymentDetails::Deposit {
117                tx_id: request.transaction_id.clone(),
118            },
119        };
120
121        Ok(Some(details))
122    }
123}
124
125impl From<SparkInvoiceDetails> for SparkInvoicePaymentDetails {
126    fn from(value: SparkInvoiceDetails) -> Self {
127        Self {
128            description: value.description,
129            invoice: value.invoice,
130        }
131    }
132}
133
134impl TryFrom<WalletTransfer> for Payment {
135    type Error = SdkError;
136    fn try_from(transfer: WalletTransfer) -> Result<Self, Self::Error> {
137        if [
138            TransferType::CounterSwap,
139            TransferType::CounterSwapV3,
140            TransferType::Swap,
141            TransferType::PrimarySwapV3,
142        ]
143        .contains(&transfer.transfer_type)
144        {
145            debug!("Tried to convert swap-related transfer to payment. Transfer: {transfer:?}");
146            return Err(SdkError::Generic(
147                "Swap-related transfers are not considered payments".to_string(),
148            ));
149        }
150        let payment_type = match transfer.direction {
151            TransferDirection::Incoming => PaymentType::Receive,
152            TransferDirection::Outgoing => PaymentType::Send,
153        };
154        let mut status = match transfer.status {
155            TransferStatus::Completed => PaymentStatus::Completed,
156            TransferStatus::SenderKeyTweaked
157                if transfer.direction == TransferDirection::Outgoing =>
158            {
159                PaymentStatus::Completed
160            }
161            TransferStatus::Expired | TransferStatus::Returned => PaymentStatus::Failed,
162            _ => PaymentStatus::Pending,
163        };
164        let (fees_sat, mut amount_sat) = match transfer.clone().user_request {
165            Some(user_request) => match user_request {
166                SspUserRequest::LightningSendRequest(r) => {
167                    // TODO: if we have the preimage it is not pending. This is a workaround
168                    // until spark will implement incremental syncing based on updated time.
169                    if r.lightning_send_payment_preimage.is_some() {
170                        status = PaymentStatus::Completed;
171                    }
172                    let fee_sat = r.fee.as_sats().unwrap_or(0);
173                    (fee_sat, transfer.total_value_sat.saturating_sub(fee_sat))
174                }
175                SspUserRequest::CoopExitRequest(r) => {
176                    let fee_sat = r
177                        .fee
178                        .as_sats()
179                        .unwrap_or(0)
180                        .saturating_add(r.l1_broadcast_fee.as_sats().unwrap_or(0));
181                    (fee_sat, transfer.total_value_sat.saturating_sub(fee_sat))
182                }
183                SspUserRequest::ClaimStaticDeposit(r) => {
184                    let fee_sat = r.max_fee.as_sats().unwrap_or(0);
185                    (fee_sat, transfer.total_value_sat)
186                }
187                _ => (0, transfer.total_value_sat),
188            },
189            None => (0, transfer.total_value_sat),
190        };
191
192        let details = PaymentDetails::from_transfer(&transfer)?;
193        if details.is_none() {
194            // in case we have a completed status without user object we want
195            // to keep syncing this payment
196            if status == PaymentStatus::Completed
197                && [
198                    TransferType::CooperativeExit,
199                    TransferType::PreimageSwap,
200                    TransferType::UtxoSwap,
201                ]
202                .contains(&transfer.transfer_type)
203            {
204                status = PaymentStatus::Pending;
205            }
206            amount_sat = transfer.total_value_sat;
207        }
208
209        Ok(Payment {
210            id: transfer.id.to_string(),
211            payment_type,
212            status,
213            amount: amount_sat.into(),
214            fees: fees_sat.into(),
215            timestamp: match transfer.created_at.map(|t| t.duration_since(UNIX_EPOCH)) {
216                Some(Ok(duration)) => duration.as_secs(),
217                _ => 0,
218            },
219            method: PaymentMethod::from_transfer(&transfer),
220            details,
221        })
222    }
223}
224
225impl Payment {
226    pub fn from_lightning(
227        payment: LightningSendPayment,
228        amount_sat: u128,
229        transfer_id: String,
230    ) -> Result<Self, SdkError> {
231        let mut status = match payment.status {
232            LightningSendStatus::LightningPaymentSucceeded => PaymentStatus::Completed,
233            LightningSendStatus::LightningPaymentFailed
234            | LightningSendStatus::TransferFailed
235            | LightningSendStatus::PreimageProvidingFailed
236            | LightningSendStatus::UserSwapReturnFailed
237            | LightningSendStatus::UserSwapReturned => PaymentStatus::Failed,
238            _ => PaymentStatus::Pending,
239        };
240        if payment.payment_preimage.is_some() {
241            status = PaymentStatus::Completed;
242        }
243
244        let invoice_details = input::parse_invoice(&payment.encoded_invoice).ok_or(
245            SdkError::Generic("Invalid invoice in LightnintSendPayment".to_string()),
246        )?;
247        let details = PaymentDetails::Lightning {
248            description: invoice_details.description,
249            preimage: payment.payment_preimage,
250            invoice: payment.encoded_invoice,
251            payment_hash: invoice_details.payment_hash,
252            destination_pubkey: invoice_details.payee_pubkey,
253            lnurl_pay_info: None,
254            lnurl_withdraw_info: None,
255            lnurl_receive_metadata: None,
256        };
257
258        Ok(Payment {
259            id: transfer_id,
260            payment_type: PaymentType::Send,
261            status,
262            amount: amount_sat,
263            fees: payment.fee_sat.into(),
264            timestamp: payment.created_at.cast_unsigned(),
265            method: PaymentMethod::Lightning,
266            details: Some(details),
267        })
268    }
269}
270
271impl From<Network> for SparkNetwork {
272    fn from(network: Network) -> Self {
273        match network {
274            Network::Mainnet => SparkNetwork::Mainnet,
275            Network::Regtest => SparkNetwork::Regtest,
276        }
277    }
278}
279
280impl From<Fee> for spark_wallet::Fee {
281    fn from(fee: Fee) -> Self {
282        match fee {
283            Fee::Fixed { amount } => spark_wallet::Fee::Fixed { amount },
284            Fee::Rate { sat_per_vbyte } => spark_wallet::Fee::Rate { sat_per_vbyte },
285        }
286    }
287}
288
289impl From<spark_wallet::TokenBalance> for TokenBalance {
290    fn from(value: spark_wallet::TokenBalance) -> Self {
291        Self {
292            balance: value.balance,
293            token_metadata: value.token_metadata.into(),
294        }
295    }
296}
297
298impl From<spark_wallet::TokenMetadata> for TokenMetadata {
299    fn from(value: spark_wallet::TokenMetadata) -> Self {
300        Self {
301            identifier: value.identifier,
302            issuer_public_key: hex::encode(value.issuer_public_key.serialize()),
303            name: value.name,
304            ticker: value.ticker,
305            decimals: value.decimals,
306            max_supply: value.max_supply,
307            is_freezable: value.is_freezable,
308        }
309    }
310}
311
312impl From<CoopExitFeeQuote> for SendOnchainFeeQuote {
313    fn from(value: CoopExitFeeQuote) -> Self {
314        Self {
315            id: value.id,
316            expires_at: value.expires_at,
317            speed_fast: value.speed_fast.into(),
318            speed_medium: value.speed_medium.into(),
319            speed_slow: value.speed_slow.into(),
320        }
321    }
322}
323
324impl From<SendOnchainFeeQuote> for CoopExitFeeQuote {
325    fn from(value: SendOnchainFeeQuote) -> Self {
326        Self {
327            id: value.id,
328            expires_at: value.expires_at,
329            speed_fast: value.speed_fast.into(),
330            speed_medium: value.speed_medium.into(),
331            speed_slow: value.speed_slow.into(),
332        }
333    }
334}
335
336impl From<CoopExitSpeedFeeQuote> for SendOnchainSpeedFeeQuote {
337    fn from(value: CoopExitSpeedFeeQuote) -> Self {
338        Self {
339            user_fee_sat: value.user_fee_sat,
340            l1_broadcast_fee_sat: value.l1_broadcast_fee_sat,
341        }
342    }
343}
344
345impl From<SendOnchainSpeedFeeQuote> for CoopExitSpeedFeeQuote {
346    fn from(value: SendOnchainSpeedFeeQuote) -> Self {
347        Self {
348            user_fee_sat: value.user_fee_sat,
349            l1_broadcast_fee_sat: value.l1_broadcast_fee_sat,
350        }
351    }
352}
353
354impl From<OnchainConfirmationSpeed> for ExitSpeed {
355    fn from(speed: OnchainConfirmationSpeed) -> Self {
356        match speed {
357            OnchainConfirmationSpeed::Fast => ExitSpeed::Fast,
358            OnchainConfirmationSpeed::Medium => ExitSpeed::Medium,
359            OnchainConfirmationSpeed::Slow => ExitSpeed::Slow,
360        }
361    }
362}
363
364impl From<ExitSpeed> for OnchainConfirmationSpeed {
365    fn from(speed: ExitSpeed) -> Self {
366        match speed {
367            ExitSpeed::Fast => OnchainConfirmationSpeed::Fast,
368            ExitSpeed::Medium => OnchainConfirmationSpeed::Medium,
369            ExitSpeed::Slow => OnchainConfirmationSpeed::Slow,
370        }
371    }
372}
373
374impl PaymentStatus {
375    pub(crate) fn from_token_transaction_status(
376        status: TokenTransactionStatus,
377        is_transfer_transaction: bool,
378    ) -> Self {
379        match status {
380            TokenTransactionStatus::Started
381            | TokenTransactionStatus::Revealed
382            | TokenTransactionStatus::Unknown => PaymentStatus::Pending,
383            TokenTransactionStatus::Signed if is_transfer_transaction => PaymentStatus::Pending,
384            TokenTransactionStatus::Finalized | TokenTransactionStatus::Signed => {
385                PaymentStatus::Completed
386            }
387            TokenTransactionStatus::StartedCancelled | TokenTransactionStatus::SignedCancelled => {
388                PaymentStatus::Failed
389            }
390        }
391    }
392}
393
394impl TryFrom<PreimageRequest> for SparkHtlcDetails {
395    type Error = SdkError;
396    fn try_from(value: PreimageRequest) -> Result<Self, Self::Error> {
397        Ok(Self {
398            payment_hash: value.payment_hash.to_string(),
399            preimage: value.preimage.map(|p| p.encode_hex()),
400            expiry_time: value
401                .expiry_time
402                .duration_since(UNIX_EPOCH)
403                .map_err(|e| SdkError::Generic(format!("Invalid expiry time: {e}")))?
404                .as_secs(),
405            status: value.status.into(),
406        })
407    }
408}
409
410impl From<PreimageRequestStatus> for SparkHtlcStatus {
411    fn from(status: PreimageRequestStatus) -> Self {
412        match status {
413            PreimageRequestStatus::WaitingForPreimage => SparkHtlcStatus::WaitingForPreimage,
414            PreimageRequestStatus::PreimageShared => SparkHtlcStatus::PreimageShared,
415            PreimageRequestStatus::Returned => SparkHtlcStatus::Returned,
416        }
417    }
418}
419
420impl From<spark_wallet::OptimizationProgress> for OptimizationProgress {
421    fn from(value: spark_wallet::OptimizationProgress) -> Self {
422        Self {
423            is_running: value.is_running,
424            current_round: value.current_round,
425            total_rounds: value.total_rounds,
426        }
427    }
428}