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