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 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 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}