breez_sdk_spark/sdk/
deposits.rs1use std::{str::FromStr, time::Duration};
2
3use bitcoin::{consensus::serialize, hex::DisplayHex};
4use platform_utils::tokio;
5use spark_wallet::{ListTransfersRequest, TransferId, WalletTransfer};
6use tracing::{error, trace};
7
8use crate::{
9 ClaimDepositRequest, ClaimDepositResponse, ListUnclaimedDepositsRequest,
10 ListUnclaimedDepositsResponse, RefundDepositRequest, RefundDepositResponse, error::SdkError,
11 models::Payment, persist::UpdateDepositPayload, sdk::RuntimeEvent,
12 utils::utxo_fetcher::CachedUtxoFetcher,
13};
14
15use super::BreezSdk;
16
17const CLAIM_TRANSFER_LOOKUP_MAX_ATTEMPTS: u32 = 3;
20const CLAIM_TRANSFER_LOOKUP_BASE_DELAY_MS: u64 = 500;
21
22#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
23#[allow(clippy::needless_pass_by_value)]
24impl BreezSdk {
25 pub async fn claim_deposit(
26 &self,
27 request: ClaimDepositRequest,
28 ) -> Result<ClaimDepositResponse, SdkError> {
29 self.maybe_ensure_spark_private_mode_initialized().await?;
30 let detailed_utxo =
31 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
32 .fetch_detailed_utxo(&request.txid, request.vout)
33 .await?;
34
35 let max_fee = request
36 .max_fee
37 .or(self.config.max_deposit_claim_fee.clone());
38 match self.claim_utxo(&detailed_utxo, max_fee).await {
39 Ok(transfer_id) => {
40 let transfer = self.lookup_claim_transfer_with_retry(transfer_id).await?;
41 let payment: Payment = transfer.try_into()?;
42 self.storage.insert_payment(payment.clone()).await?;
45 self.storage
46 .delete_deposit(detailed_utxo.txid.to_string(), detailed_utxo.vout)
47 .await?;
48 self.event_emitter
49 .emit_runtime_event(RuntimeEvent::DepositClaimed {
50 payment: Box::new(payment.clone()),
51 })
52 .await;
53 Ok(ClaimDepositResponse { payment })
54 }
55 Err(e) => {
56 error!("Failed to claim deposit: {e:?}");
57 self.storage
58 .update_deposit(
59 detailed_utxo.txid.to_string(),
60 detailed_utxo.vout,
61 UpdateDepositPayload::ClaimError {
62 error: e.clone().into(),
63 },
64 )
65 .await?;
66 Err(e)
67 }
68 }
69 }
70
71 pub async fn refund_deposit(
72 &self,
73 request: RefundDepositRequest,
74 ) -> Result<RefundDepositResponse, SdkError> {
75 let detailed_utxo =
76 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
77 .fetch_detailed_utxo(&request.txid, request.vout)
78 .await?;
79 let tx = self
80 .spark_wallet
81 .refund_static_deposit(
82 detailed_utxo.clone().tx,
83 Some(detailed_utxo.vout),
84 &request.destination_address,
85 request.fee.into(),
86 )
87 .await?;
88 let tx_hex = serialize(&tx).as_hex().to_string();
89 let tx_id = tx.compute_txid().as_raw_hash().to_string();
90
91 self.storage
93 .update_deposit(
94 detailed_utxo.txid.to_string(),
95 detailed_utxo.vout,
96 UpdateDepositPayload::Refund {
97 refund_tx: tx_hex.clone(),
98 refund_txid: tx_id.clone(),
99 },
100 )
101 .await?;
102
103 self.chain_service
104 .broadcast_transaction(tx_hex.clone())
105 .await?;
106 Ok(RefundDepositResponse { tx_id, tx_hex })
107 }
108
109 #[allow(unused_variables)]
110 pub async fn list_unclaimed_deposits(
111 &self,
112 request: ListUnclaimedDepositsRequest,
113 ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
114 let deposits = self.storage.list_deposits().await?;
115 Ok(ListUnclaimedDepositsResponse { deposits })
116 }
117}
118
119impl BreezSdk {
120 async fn lookup_claim_transfer_with_retry(
127 &self,
128 transfer_id: String,
129 ) -> Result<WalletTransfer, SdkError> {
130 let parsed_id = TransferId::from_str(&transfer_id).map_err(SdkError::Generic)?;
131 let mut last_error: Option<SdkError> = None;
132
133 for attempt in 0..CLAIM_TRANSFER_LOOKUP_MAX_ATTEMPTS {
134 if attempt > 0 {
135 let delay_ms = CLAIM_TRANSFER_LOOKUP_BASE_DELAY_MS
136 .saturating_mul(2u64.saturating_pow(attempt.saturating_sub(1)));
137 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
138 trace!(
139 "Retrying claim transfer lookup (attempt {}/{}) for transfer {transfer_id}",
140 attempt.saturating_add(1),
141 CLAIM_TRANSFER_LOOKUP_MAX_ATTEMPTS
142 );
143 }
144
145 match self
146 .spark_wallet
147 .list_transfers(ListTransfersRequest {
148 transfer_ids: vec![parsed_id.clone()],
149 paging: None,
150 })
151 .await
152 {
153 Ok(mut resp) => {
154 if let Some(transfer) = resp.items.pop() {
155 return Ok(transfer);
156 }
157 last_error = None;
158 }
159 Err(e) => last_error = Some(e.into()),
160 }
161 }
162
163 Err(last_error
164 .unwrap_or_else(|| SdkError::Generic("transfer not found after claim".to_string())))
165 }
166}