spark_sdk/wallet/handlers/
transfer.rs

1use crate::{
2    constants::spark::DEFAULT_TRANSFER_EXPIRY,
3    error::SparkSdkError,
4    signer::traits::{secp256k1::KeygenMethod, SparkSigner},
5    wallet::{
6        internal_handlers::traits::{
7            leaves::LeavesInternalHandlers,
8            transfer::{LeafKeyTweak, TransferInternalHandlers},
9        },
10        leaf_manager::LeafNodeStatus,
11    },
12    SparkSdk,
13};
14use spark_protos::spark::{
15    query_pending_transfers_request::Participant, QueryPendingTransfersRequest, Transfer,
16    TransferStatus,
17};
18use uuid::Uuid;
19
20impl<S: SparkSigner + Send + Sync + Clone + 'static> SparkSdk<S> {
21    /// Queries all pending transfers where the current user is the receiver.
22    ///
23    /// This function retrieves all pending transfers that are waiting to be accepted by the current user.
24    /// A pending transfer represents funds that have been sent to the user but have not yet been claimed.
25    /// The transfers remain in a pending state until the receiver claims them, at which point the funds
26    /// become available in their wallet.
27    ///
28    /// # Returns
29    ///
30    /// * `Ok(Vec<Transfer>)` - A vector of pending `Transfer` objects if successful
31    /// * `Err(SparkSdkError)` - If there was an error querying the transfers
32    ///
33    /// # Example
34    ///
35    /// ```no_run
36    /// # use spark_wallet_sdk::SparkSdk;
37    /// # async fn example(sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
38    /// let pending = sdk.query_pending_transfers().await?;
39    /// for transfer in pending {
40    ///     println!("Pending transfer: {} satoshis", transfer.amount);
41    /// }
42    /// # Ok(())
43    /// # }
44    /// ```
45    pub async fn query_pending_transfers(&self) -> Result<Vec<Transfer>, SparkSdkError> {
46        let mut spark_client = self.config.spark_config.get_spark_connection(None).await?;
47
48        let mut request = tonic::Request::new(QueryPendingTransfersRequest {
49            transfer_ids: vec![],
50            participant: Some(Participant::ReceiverIdentityPublicKey(
51                self.get_identity_public_key().to_vec(),
52            )),
53        });
54        self.add_authorization_header_to_request(&mut request, None);
55
56        let response = spark_client
57            .query_pending_transfers(request)
58            .await?
59            .into_inner();
60
61        Ok(response.transfers)
62    }
63
64    /// Initiates a transfer of funds to another user.
65    ///
66    /// This function handles the process of transferring funds from the current user's wallet to another user,
67    /// identified by their public key. The transfer process involves several steps:
68    ///
69    /// 1. Selecting appropriate leaves (UTXOs) that contain sufficient funds for the transfer
70    /// 2. Locking the selected leaves to prevent concurrent usage
71    /// 3. Generating new signing keys for the transfer
72    /// 4. Creating and signing the transfer transaction
73    /// 5. Removing the used leaves from the wallet
74    ///
75    /// The transfer remains in a pending state until the receiver claims it. The expiry time is set to
76    /// 30 days by default (see `DEFAULT_TRANSFER_EXPIRY`).
77    ///
78    /// # Arguments
79    ///
80    /// * `amount` - The amount to transfer in satoshis. Must be greater than the dust limit and the wallet
81    ///             must have a leaf with exactly this amount.
82    /// * `receiver_identity_pubkey` - The public key identifying the receiver of the transfer. This should
83    ///                               be the receiver's identity public key, not a regular Bitcoin public key.
84    ///
85    /// # Returns
86    ///
87    /// * `Ok(String)` - The transfer ID if successful. This ID can be used to track the transfer status.
88    /// * `Err(SparkSdkError)` - If the transfer fails. Common error cases include:
89    ///   - No leaf with exact amount available
90    ///   - Failed to lock leaves
91    ///   - Failed to generate new signing keys
92    ///   - Network errors when communicating with Spark operators
93    ///
94    /// # Example
95    ///
96    /// ```no_run
97    /// # use spark_wallet_sdk::SparkSdk;
98    /// # async fn example(sdk: &mut SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
99    /// let amount = 100_000; // 100k satoshis
100    /// let receiver_pubkey = vec![/* receiver's public key bytes */];
101    ///
102    /// let transfer_id = sdk.transfer(amount, receiver_pubkey).await?;
103    /// println!("Transfer initiated with ID: {}", transfer_id);
104    /// # Ok(())
105    /// # }
106    /// ```
107    ///
108    /// # Notes
109    ///
110    /// Currently, the leaf selection algorithm only supports selecting a single leaf with the exact
111    /// transfer amount. Future versions will support combining multiple leaves and handling change outputs.
112    pub async fn transfer(
113        &self,
114        amount: u64,
115        receiver_identity_pubkey: Vec<u8>,
116    ) -> Result<String, SparkSdkError> {
117        // TODO: leaf selection currently only returns one leaf. It must be changed to return multiple leaves.
118        let expiry_time = chrono::Utc::now().timestamp() as u64 + DEFAULT_TRANSFER_EXPIRY;
119        let transfer_request_id = Uuid::now_v7().to_string(); // TODO: remove this.
120
121        // do leaf selection
122        // if not sufficient leaves are found, a swap with the SSP will be requested
123        let leaf_selection_response = self
124            .prepare_leaves_for_amount(amount, None, LeafNodeStatus::InTransfer)
125            .await?;
126
127        let unlocking_id = leaf_selection_response.unlocking_id.unwrap();
128
129        // TODO: expect that at this point, leaf_selection_response.total_value == amount, because a swap should happen between the SSP and the wallet.
130        let selected_leaves = leaf_selection_response.leaves;
131        let leaf_ids = selected_leaves
132            .iter()
133            .map(|leaf| leaf.id.clone())
134            .collect::<Vec<String>>();
135
136        let mut leaves_to_transfer = Vec::new();
137        for leaf in selected_leaves {
138            let new_signing_public_key = self
139                .signer
140                .new_secp256k1_keypair(KeygenMethod::Uuid(leaf.id.clone()))?;
141
142            leaves_to_transfer.push(LeafKeyTweak {
143                leaf: leaf.marshal_to_tree_node(
144                    self.get_identity_public_key(),
145                    &self.config.spark_config.network,
146                ),
147                old_signing_private_key: self
148                    .signer
149                    .sensitive_expose_secret_key_from_pubkey(&leaf.signing_public_key, false)?,
150                new_signing_public_key: new_signing_public_key.to_vec(),
151            });
152        }
153
154        // TODO - when add actual leaf selection, this might be an array of length > 1.
155        let transfer = self
156            .start_send_transfer(&leaves_to_transfer, receiver_identity_pubkey, expiry_time)
157            .await?;
158
159        // unlock and remove leaves from the leaf manager
160        self.leaf_manager
161            .delete_leaves_after_transfer(&unlocking_id, &leaf_ids)
162            .await?;
163
164        Ok(transfer.id)
165    }
166
167    pub async fn transfer_leaf_ids(
168        &self,
169        leaf_ids: Vec<String>,
170        receiver_identity_pubkey: Vec<u8>,
171    ) -> Result<String, SparkSdkError> {
172        let expiry_time = chrono::Utc::now().timestamp() as u64 + DEFAULT_TRANSFER_EXPIRY;
173        let transfer_request_id = Uuid::now_v7().to_string();
174
175        let leaf_selection_response = self
176            .verify_and_get_leaves(leaf_ids.clone(), LeafNodeStatus::InTransfer)
177            .await?;
178
179        let unlocking_id = leaf_selection_response.unlocking_id.unwrap();
180
181        let selected_leaves = leaf_selection_response.leaves;
182
183        let mut leaves_to_transfer = Vec::new();
184        for leaf in selected_leaves {
185            let new_signing_public_key = self
186                .signer
187                .new_secp256k1_keypair(KeygenMethod::Uuid(leaf.id.clone()))?;
188
189            leaves_to_transfer.push(LeafKeyTweak {
190                leaf: leaf.marshal_to_tree_node(
191                    self.get_identity_public_key(),
192                    &self.config.spark_config.network,
193                ),
194                old_signing_private_key: self
195                    .signer
196                    .sensitive_expose_secret_key_from_pubkey(&leaf.signing_public_key, false)?,
197                new_signing_public_key: new_signing_public_key.to_vec(),
198            });
199        }
200
201        // TODO - when add actual leaf selection, this might be an array of length > 1.
202        let transfer = self
203            .start_send_transfer(&leaves_to_transfer, receiver_identity_pubkey, expiry_time)
204            .await?;
205
206        // unlock and remove leaves from the leaf manager
207        self.leaf_manager
208            .delete_leaves_after_transfer(&unlocking_id, &leaf_ids)
209            .await?;
210
211        Ok(transfer.id)
212    }
213
214    /// Claims a pending transfer that was sent to this wallet.
215    ///
216    /// This function processes a pending transfer and claims the funds into the wallet. It performs the following steps:
217    /// 1. Verifies the transfer is in the correct state (SenderKeyTweaked)
218    /// 2. Verifies and decrypts the leaf private keys using the wallet's identity key
219    /// 3. Generates new signing keys for the claimed leaves
220    /// 4. Finalizes the transfer by:
221    ///    - Tweaking the leaf keys
222    ///    - Signing refund transactions
223    ///    - Submitting the signatures to the Spark network
224    ///    - Storing the claimed leaves in the wallet's database
225    ///
226    /// # Arguments
227    ///
228    /// * `transfer` - The pending transfer to claim, must be in SenderKeyTweaked status
229    ///
230    /// # Returns
231    ///
232    /// * `Ok(())` - If the transfer was successfully claimed
233    /// * `Err(SparkSdkError)` - If there was an error during the claim process
234    ///
235    /// # Errors
236    ///
237    /// Returns [`SparkSdkError::InvalidInput`] if:
238    /// - The transfer is not in SenderKeyTweaked status
239    ///
240    /// May also return other `SparkSdkError` variants for network, signing or storage errors.
241    ///
242    /// # Example
243    ///
244    /// ```no_run
245    /// # use spark_wallet_sdk::SparkSdk;
246    /// # async fn example(mut sdk: SparkSdk) -> Result<(), Box<dyn std::error::Error>> {
247    /// let pending = sdk.query_pending_transfers().await?;
248    /// for transfer in pending {
249    ///     sdk.claim_transfer(&transfer).await?;
250    /// }
251    /// # Ok(())
252    /// # }
253    /// ```
254    pub async fn claim_transfer(&self, transfer: Transfer) -> Result<(), SparkSdkError> {
255        if transfer.status != TransferStatus::SenderKeyTweaked as i32 {
256            return Err(SparkSdkError::InvalidInput(
257                "Transfer is not in the correct status".into(),
258            ));
259        }
260
261        let mut leaves_to_claim = Vec::new();
262        for leaf in &transfer.leaves {
263            let leaf_private_key_map = self.verify_pending_transfer(&transfer).await?;
264
265            let leaf_id = leaf.leaf.as_ref().unwrap().id.clone();
266            let new_pubkey = self
267                .signer
268                .new_secp256k1_keypair(KeygenMethod::Uuid(leaf_id.clone()))?
269                .to_vec();
270
271            self.signer
272                .insert_secp256k1_keypair_from_secret_key(
273                    &leaf_private_key_map[&leaf.leaf.as_ref().unwrap().id],
274                )
275                .unwrap();
276
277            let claim_node = LeafKeyTweak {
278                leaf: leaf.leaf.as_ref().unwrap().clone(),
279                old_signing_private_key: leaf_private_key_map[&leaf_id].to_vec(),
280                new_signing_public_key: new_pubkey.to_vec(),
281            };
282
283            leaves_to_claim.push(claim_node);
284        }
285
286        self.claim_finalize_incoming_transfer(&transfer, &leaves_to_claim)
287            .await?;
288
289        Ok(())
290    }
291
292    pub async fn claim_transfers(&self) -> Result<(), SparkSdkError> {
293        let pending = self.query_pending_transfers().await?;
294        let claim_futures = pending.into_iter().map(|transfer| {
295            let transfer_clone = transfer.clone();
296            self.claim_transfer(transfer_clone)
297        });
298        futures::future::try_join_all(claim_futures).await?;
299        Ok(())
300    }
301}