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