spark_rust/wallet/handlers/
deposit.rs

1use std::collections::HashMap;
2
3use bitcoin::secp256k1::PublicKey;
4use spark_protos::spark::{
5    DepositAddressQueryResult, GenerateDepositAddressRequest, QueryUnusedDepositAddressesRequest,
6    TreeNode,
7};
8use tonic::Request;
9
10use crate::{
11    error::{NetworkError, SparkSdkError, ValidationError},
12    signer::traits::SparkSigner,
13    wallet::{
14        internal_handlers::{
15            implementations::deposit::Address,
16            traits::{deposit::DepositInternalHandlers, mempool::MempoolInternalHandlers},
17        },
18        utils::bitcoin::serialize_bitcoin_transaction,
19    },
20    SparkSdk,
21};
22
23/// The response from the [`SparkSdk::generate_deposit_address`] method.
24pub struct GenerateDepositAddressSdkResponse {
25    /// The deposit address, as [``bitcoin::Address``]. Deposit addresses are P2TR addresses.
26    pub deposit_address: bitcoin::Address,
27
28    /// The signing public key for this deposit address, as [``bitcoin::secp256k1::PublicKey``].
29    pub signing_public_key: PublicKey,
30
31    /// The verifying public key as [``bitcoin::secp256k1::PublicKey``]. *Note:* this is an interpolated key used to verify that threshold signatures between the user and the Spark Operators are valid. This should *not* by used for signing.
32    pub verifying_public_key: PublicKey,
33}
34
35impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
36    /// Generates a new deposit address for receiving funds into the Spark wallet.
37    ///
38    /// This function handles the generation of a new deposit address by:
39    /// 1. Creating a new signing keypair for the deposit address
40    /// 2. Requesting a deposit address from the Spark network
41    /// 3. Validating the returned address and proof of possession
42    ///
43    /// # Returns
44    ///
45    /// * `Ok(GenerateDepositAddressSdkResponse)` - Contains the validated deposit address and signing public key
46    /// * `Err(SparkSdkError)` - If there was an error during address generation
47    ///
48    /// # Errors
49    ///
50    /// Returns [`SparkSdkError`] if:
51    /// - Failed to generate new signing keypair
52    /// - Network errors when communicating with Spark operators
53    /// - Address validation fails (e.g. invalid proof of possession)
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// # use spark_rust::SparkSdk;
59    /// # async fn example(mut sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
60    /// let deposit_address = sdk.generate_deposit_address().await?;
61    /// println!("New deposit address: {}", deposit_address.deposit_address);
62    /// # Ok(())
63    /// # }
64    /// ```
65    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
66    pub async fn generate_deposit_address(
67        &self,
68    ) -> Result<GenerateDepositAddressSdkResponse, SparkSdkError> {
69        let identity_public_key = self.get_spark_address()?;
70
71        let signing_public_key = self
72            .signer
73            .get_deposit_signing_key(self.config.spark_config.network.to_bitcoin_network())?;
74
75        let mut spark_client = self.config.spark_config.get_spark_connection(None).await?;
76        let mut request = tonic::Request::new(GenerateDepositAddressRequest {
77            signing_public_key: signing_public_key.serialize().to_vec(),
78            identity_public_key: identity_public_key.serialize().to_vec(),
79            network: self.config.spark_config.network.marshal_proto(),
80        });
81        self.add_authorization_header_to_request(&mut request, None);
82
83        let response = spark_client
84            .generate_deposit_address(request)
85            .await
86            .map_err(|status| SparkSdkError::from(NetworkError::Status(status)))?;
87
88        let response = response.into_inner();
89        let address = Address::try_from_address(
90            response
91                .deposit_address
92                .ok_or(SparkSdkError::from(NetworkError::InvalidResponse))?,
93            self.config.spark_config.network,
94        )?;
95
96        self.validate_deposit_address(&address, &signing_public_key)
97            .await?;
98
99        Ok(GenerateDepositAddressSdkResponse {
100            deposit_address: address.address,
101            signing_public_key,
102            verifying_public_key: address.verifying_key,
103        })
104    }
105
106    /// Claims a pending deposit by txid
107    /// 1. Querying if the txid is a pending deposit
108    /// 2. Checking the mempool for transaction
109    /// 3. Finalizing the deposit by creating tree nodes
110    ///
111    /// # Errors
112    ///
113    /// Returns [`SparkSdkError`] if:
114    /// - Failed to connect to Spark service
115    /// - Failed to query mempool
116    /// - Failed to finalize deposits
117    ///
118    /// # Example
119    ///
120    /// ```
121    /// # use spark_rust::{SparkSdk, SparkNetwork, signer::default_signer::DefaultSigner, signer::traits::SparkSigner};
122    ///
123    /// async fn example() {
124    ///     let mnemonic = "abandon ability able about above absent absorb abstract absurd abuse access accident";
125    ///     let network = SparkNetwork::Regtest;
126    ///     let signer = DefaultSigner::from_mnemonic(mnemonic, network.clone()).await.unwrap();
127    ///     let sdk = SparkSdk::new(network, signer).await.unwrap();
128    ///
129    ///     let hardcoded_txid = "edb5575e6ee96fcf175c9114e0b0d86d99d4642956edcd02e6ec7b6899e90b41";
130    ///     sdk.claim_deposit(hardcoded_txid.to_string()).await.unwrap();
131    /// }
132    /// ```
133    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
134    pub async fn claim_deposit(&self, txid: String) -> Result<Vec<TreeNode>, SparkSdkError> {
135        // query the transaction in the mempool
136        let deposit_tx = self.query_mempool_transaction_by_txid(txid).await?;
137
138        // query unused deposit addresses from Spark
139        let time_start = std::time::Instant::now();
140        let mut spark_client = self.config.spark_config.get_spark_connection(None).await?;
141        let identity_public_key = self.get_spark_address();
142        let mut request = Request::new(QueryUnusedDepositAddressesRequest {
143            identity_public_key: identity_public_key?.serialize().to_vec(),
144        });
145        self.add_authorization_header_to_request(&mut request, None);
146        let response_outer = spark_client
147            .query_unused_deposit_addresses(request)
148            .await
149            .map_err(|status| SparkSdkError::from(NetworkError::Status(status)))?;
150        let deposit_addresses = response_outer.into_inner().deposit_addresses;
151
152        let duration = time_start.elapsed();
153        #[cfg(feature = "telemetry")]
154        tracing::debug!(duration = ?duration, "query_unused_deposit_addresses");
155
156        // create a hashmap from an iterator that maps the returned Spark deposit addresses to the user's address
157        let unused_deposit_addresses: HashMap<String, DepositAddressQueryResult> =
158            deposit_addresses
159                .iter()
160                .map(|deposit_address| {
161                    (
162                        deposit_address.deposit_address.clone(),
163                        deposit_address.clone(),
164                    )
165                })
166                .collect();
167
168        let mut vout: u32 = 0;
169        let mut found_deposit_data: Option<DepositAddressQueryResult> = None;
170
171        for (i, tx_output) in deposit_tx.output.iter().enumerate() {
172            let parsed_script = tx_output.script_pubkey.clone();
173            let script_address = bitcoin::Address::from_script(
174                &parsed_script,
175                self.config.spark_config.network.to_bitcoin_network(),
176            )
177            .ok()
178            .map(|addr| addr.to_string())
179            .ok_or_else(|| {
180                SparkSdkError::from(ValidationError::InvalidAddress {
181                    address: "Invalid address".to_string(),
182                })
183            })?;
184
185            if unused_deposit_addresses.contains_key(&script_address) {
186                // Found a matching deposit address
187                vout = i as u32;
188                found_deposit_data = Some(
189                    unused_deposit_addresses
190                        .get(&script_address)
191                        .unwrap()
192                        .clone(),
193                );
194
195                #[cfg(feature = "telemetry")]
196                tracing::debug!(address = script_address, "Found matching deposit address");
197
198                break;
199            }
200        }
201
202        // If no matching deposit address is found, return an error
203        let deposit_data = found_deposit_data.ok_or_else(|| {
204            SparkSdkError::from(ValidationError::InvalidAddress {
205                address: "Deposit address not found".to_string(),
206            })
207        })?;
208
209        let nodes = self
210            .finalize_deposit(
211                deposit_data.user_signing_public_key.clone(),
212                deposit_data.verifying_public_key.clone(),
213                deposit_tx.clone(),
214                vout,
215            )
216            .await?;
217
218        self.claim_transfers().await?;
219
220        Ok(nodes)
221    }
222
223    /// Finalizes a deposit by creating a tree node and transferring it to self
224    ///
225    /// # Arguments
226    ///
227    /// * `signing_pubkey` - The public key used for signing
228    /// * `deposit_tx` - The Bitcoin transaction containing the deposit
229    /// * `vout` - The output index in the transaction
230    ///
231    /// # Errors
232    ///
233    /// Returns [`SparkSdkError`] if:
234    /// - Failed to create tree node
235    /// - Failed to transfer deposits
236    ///
237    /// # Returns
238    ///
239    /// Returns an empty vector of `TreeNode`s on success
240    pub async fn finalize_deposit(
241        &self,
242        signing_pubkey: Vec<u8>,
243        verifying_pubkey: Vec<u8>,
244        deposit_tx: bitcoin::Transaction,
245        vout: u32,
246    ) -> Result<Vec<TreeNode>, SparkSdkError> {
247        let time_start = std::time::Instant::now();
248        let created_root = self
249            .create_tree_root(
250                signing_pubkey,
251                verifying_pubkey,
252                serialize_bitcoin_transaction(&deposit_tx)?,
253                vout,
254            )
255            .await?;
256        let duration = time_start.elapsed();
257
258        #[cfg(feature = "telemetry")]
259        tracing::debug!(duration = ?duration, "finalize_deposit");
260
261        // Transfer the leaves to self.
262        let time_start = std::time::Instant::now();
263        self.transfer_deposits_to_self(
264            created_root
265                .nodes
266                .iter()
267                .map(|node| node.id.clone())
268                .collect(),
269        )
270        .await?;
271
272        let duration = time_start.elapsed();
273        #[cfg(feature = "telemetry")]
274        tracing::debug!(duration = ?duration, "transfer_deposits_to_self");
275
276        Ok(created_root.nodes)
277    }
278
279    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
280    pub async fn query_unused_deposit_addresses(
281        &self,
282    ) -> Result<Vec<DepositAddressQueryResult>, SparkSdkError> {
283        let identity_public_key = self.get_spark_address()?;
284        let mut spark_client = self.config.spark_config.get_spark_connection(None).await?;
285        let mut request = Request::new(QueryUnusedDepositAddressesRequest {
286            identity_public_key: identity_public_key.serialize().to_vec(),
287        });
288        self.add_authorization_header_to_request(&mut request, None);
289        let response = spark_client
290            .query_unused_deposit_addresses(request)
291            .await
292            .map_err(|status| SparkSdkError::from(NetworkError::Status(status)))?;
293
294        let response = response.into_inner();
295        Ok(response.deposit_addresses)
296    }
297
298    #[cfg_attr(feature = "telemetry", tracing::instrument(skip_all))]
299    async fn transfer_deposits_to_self(&self, leaf_ids: Vec<String>) -> Result<(), SparkSdkError> {
300        self.transfer_leaf_ids(leaf_ids, &self.get_spark_address()?)
301            .await?;
302
303        Ok(())
304    }
305}