breez_sdk_spark/models/
adaptors.rs

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