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 let should_emit_event = self.storage.apply_payment_update(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 should_emit_event,
52 })
53 .await;
54 Ok(ClaimDepositResponse { payment })
55 }
56 Err(e) => {
57 error!("Failed to claim deposit: {e:?}");
58 self.storage
59 .update_deposit(
60 detailed_utxo.txid.to_string(),
61 detailed_utxo.vout,
62 UpdateDepositPayload::ClaimError {
63 error: e.clone().into(),
64 },
65 )
66 .await?;
67 Err(e)
68 }
69 }
70 }
71
72 pub async fn refund_deposit(
73 &self,
74 request: RefundDepositRequest,
75 ) -> Result<RefundDepositResponse, SdkError> {
76 let detailed_utxo =
77 CachedUtxoFetcher::new(self.chain_service.clone(), self.storage.clone())
78 .fetch_detailed_utxo(&request.txid, request.vout)
79 .await?;
80 let tx = self
81 .spark_wallet
82 .refund_static_deposit(
83 detailed_utxo.clone().tx,
84 Some(detailed_utxo.vout),
85 &request.destination_address,
86 request.fee.into(),
87 )
88 .await?;
89 let tx_hex = serialize(&tx).as_hex().to_string();
90 let tx_id = tx.compute_txid().as_raw_hash().to_string();
91
92 self.storage
94 .update_deposit(
95 detailed_utxo.txid.to_string(),
96 detailed_utxo.vout,
97 UpdateDepositPayload::Refund {
98 refund_tx: tx_hex.clone(),
99 refund_txid: tx_id.clone(),
100 },
101 )
102 .await?;
103
104 self.chain_service
105 .broadcast_transaction(tx_hex.clone())
106 .await?;
107 Ok(RefundDepositResponse { tx_id, tx_hex })
108 }
109
110 #[allow(unused_variables)]
111 pub async fn list_unclaimed_deposits(
112 &self,
113 request: ListUnclaimedDepositsRequest,
114 ) -> Result<ListUnclaimedDepositsResponse, SdkError> {
115 let deposits = self.storage.list_deposits().await?;
116 Ok(ListUnclaimedDepositsResponse { deposits })
117 }
118}
119
120impl BreezSdk {
121 async fn lookup_claim_transfer_with_retry(
128 &self,
129 transfer_id: String,
130 ) -> Result<WalletTransfer, SdkError> {
131 let parsed_id = TransferId::from_str(&transfer_id).map_err(SdkError::Generic)?;
132 let mut last_error: Option<SdkError> = None;
133
134 for attempt in 0..CLAIM_TRANSFER_LOOKUP_MAX_ATTEMPTS {
135 if attempt > 0 {
136 let delay_ms = CLAIM_TRANSFER_LOOKUP_BASE_DELAY_MS
137 .saturating_mul(2u64.saturating_pow(attempt.saturating_sub(1)));
138 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
139 trace!(
140 "Retrying claim transfer lookup (attempt {}/{}) for transfer {transfer_id}",
141 attempt.saturating_add(1),
142 CLAIM_TRANSFER_LOOKUP_MAX_ATTEMPTS
143 );
144 }
145
146 match self
147 .spark_wallet
148 .list_transfers(ListTransfersRequest {
149 transfer_ids: vec![parsed_id.clone()],
150 paging: None,
151 })
152 .await
153 {
154 Ok(mut resp) => {
155 if let Some(transfer) = resp.items.pop() {
156 return Ok(transfer);
157 }
158 last_error = None;
159 }
160 Err(e) => last_error = Some(e.into()),
161 }
162 }
163
164 Err(last_error
165 .unwrap_or_else(|| SdkError::Generic("transfer not found after claim".to_string())))
166 }
167}