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