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}